/*
 Navicat Premium Data Transfer

 Source Server         : Ali
 Source Server Type    : MySQL
 Source Server Version : 50728
 Source Host           : 123.57.140.84:3306
 Source Schema         : My_Blog_db

 Target Server Type    : MySQL
 Target Server Version : 50728
 File Encoding         : 65001

 Date: 01/04/2022 11:38:18
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu`  (
  `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜单名称',
  `parent_id` bigint(20) NULL DEFAULT 0 COMMENT '父菜单ID',
  `order_num` int(4) NULL DEFAULT 0 COMMENT '显示顺序',
  `path` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '路由地址',
  `component` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组件路径',
  `is_frame` int(1) NULL DEFAULT 1 COMMENT '是否为外链（0是 1否）',
  `menu_type` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '菜单类型（M目录 C菜单 F按钮）',
  `visible` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '菜单状态（0显示 1隐藏）',
  `perms` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '#' COMMENT '菜单图标',
  `remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '备注',
  `create_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2019 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (1, '系统管理', 0, 1, 'system', NULL, 1, 'M', '0', '', 'system', '系统管理目录', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (2, '内容管理', 0, 2, 'content', NULL, 1, 'M', '0', '', 'guide', '系统内容目录', '2018-03-16 11:33:00', '2020-03-30 16:02:35');
INSERT INTO `sys_menu` VALUES (100, '用户管理', 1, 1, 'user', 'system/user/index', 1, 'C', '0', 'system:user:list', 'user', '用户管理菜单', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (101, '角色管理', 1, 2, 'role', 'system/role/index', 1, 'C', '0', 'system:role:list', 'peoples', '角色管理菜单', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (102, '博客管理', 2, 1, 'blog', 'content/blog/page', 1, 'M', '0', 'content:blog:list', 'tree', '博客管理菜单', '2018-03-16 11:33:00', '2020-04-01 10:53:35');
INSERT INTO `sys_menu` VALUES (103, '标签管理', 2, 2, 'tag', 'content/tag/index', 1, 'C', '0', 'content:tag:list', 'post', '标签管理菜单', '2018-03-16 11:33:00', '2020-04-01 10:25:09');
INSERT INTO `sys_menu` VALUES (104, '类型管理', 2, 3, 'type', 'content/type/index', 1, 'C', '0', 'content:type:list', 'dict', '类型管理菜单', '2018-03-16 11:33:00', '2020-04-01 10:25:12');
INSERT INTO `sys_menu` VALUES (105, '友链管理', 2, 4, 'friend', 'content/friend/index', 1, 'C', '0', 'content:friend:list', 'wechat', '友链管理菜单', '2018-03-16 11:33:00', '2020-04-01 10:25:14');
INSERT INTO `sys_menu` VALUES (106, '评论管理', 2, 5, 'comment', 'content/comment/index', 1, 'C', '0', 'content:comment:list', 'message', '评论管理菜单', '2018-03-16 11:33:00', '2020-04-01 10:25:16');
INSERT INTO `sys_menu` VALUES (107, '日志查看', 2, 6, 'requestlog', 'content/requestlog/index', 1, 'C', '0', 'content:requestlog:list', 'log', '日志管理菜单', '2020-04-01 09:59:02', '2020-04-01 10:25:23');
INSERT INTO `sys_menu` VALUES (108, '博客查询', 102, 1, 'index', 'content/blog/index', 1, 'C', '0', 'content:blog:query', 'nested', '博客列表', '2020-03-30 19:43:00', '2020-04-01 11:06:07');
INSERT INTO `sys_menu` VALUES (109, '博客添加', 102, 2, 'add', 'content/blog/add', 1, 'C', '0', 'content:blog:add', 'form', '博客添加', '2020-03-30 19:32:48', '2020-04-01 10:25:43');
INSERT INTO `sys_menu` VALUES (110, '博客修改', 102, 3, 'edit', 'content/blog/edit', 1, 'C', '0', 'content:blog:edit', 'edit', '博客修改', '2020-03-30 15:44:28', '2020-04-01 10:25:27');
INSERT INTO `sys_menu` VALUES (111, '博客删除', 102, 4, '', NULL, 1, 'F', '0', 'content:blog:remove', '#', '', '2020-04-01 10:51:56', '2020-04-01 10:52:16');
INSERT INTO `sys_menu` VALUES (112, '博客状态', 102, 5, '', NULL, 1, 'F', '0', 'content:blog:setPub', '#', '', '2020-04-01 10:56:43', '2020-04-01 10:56:43');
INSERT INTO `sys_menu` VALUES (1001, '用户查询', 100, 1, '', '', 1, 'F', '0', 'system:user:query', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1002, '用户新增', 100, 2, '', '', 1, 'F', '0', 'system:user:add', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1003, '用户修改', 100, 3, '', '', 1, 'F', '0', 'system:user:edit', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1004, '用户删除', 100, 4, '', '', 1, 'F', '0', 'system:user:remove', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1005, '重置密码', 100, 5, '', '', 1, 'F', '0', 'system:user:resetPwd', '#', '', '2018-03-16 11:33:00', '2020-03-31 15:48:32');
INSERT INTO `sys_menu` VALUES (1008, '角色查询', 101, 1, '', '', 1, 'F', '0', 'system:role:query', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1009, '角色新增', 101, 2, '', '', 1, 'F', '0', 'system:role:add', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1010, '角色修改', 101, 3, '', '', 1, 'F', '0', 'system:role:edit', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1011, '角色删除', 101, 4, '', '', 1, 'F', '0', 'system:role:remove', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (2003, '标签查询', 103, 1, '', NULL, 1, 'F', '0', 'content:tag:query', '#', '', '2020-04-01 10:29:04', '2020-04-01 10:29:20');
INSERT INTO `sys_menu` VALUES (2004, '标签新增', 103, 2, '', NULL, 1, 'F', '0', 'content:tag:add', '#', '', '2020-04-01 10:29:44', '2020-04-01 10:30:05');
INSERT INTO `sys_menu` VALUES (2005, '标签修改', 103, 3, '', NULL, 1, 'F', '0', 'content:tag:edit', '#', '', '2020-04-01 10:29:59', '2020-04-01 10:30:25');
INSERT INTO `sys_menu` VALUES (2006, '标签删除', 103, 4, '', NULL, 1, 'F', '0', 'content:tag:remove', '#', '', '2020-04-01 10:30:23', '2020-04-01 10:30:40');
INSERT INTO `sys_menu` VALUES (2007, '类型查询', 104, 1, '', NULL, 1, 'F', '0', 'content:type:query', '#', '', '2020-04-01 10:31:05', '2020-04-01 10:31:15');
INSERT INTO `sys_menu` VALUES (2008, '类型新增', 104, 2, '', NULL, 1, 'F', '0', 'content:type:add', '#', '', '2020-04-01 10:31:37', '2020-04-01 10:31:37');
INSERT INTO `sys_menu` VALUES (2009, '类型修改', 104, 3, '', NULL, 1, 'F', '0', 'content:type:edit', '#', '', '2020-04-01 10:31:53', '2020-04-01 10:31:53');
INSERT INTO `sys_menu` VALUES (2010, '类型删除', 104, 4, '', NULL, 1, 'F', '0', 'content:type:remove', '#', '', '2020-04-01 10:32:09', '2020-04-01 10:32:09');
INSERT INTO `sys_menu` VALUES (2011, '友链查询', 105, 1, '', NULL, 1, 'F', '0', 'content:friend:query', '#', '', '2020-04-01 10:32:49', '2020-04-01 10:32:58');
INSERT INTO `sys_menu` VALUES (2012, '友链新增', 105, 2, '', NULL, 1, 'F', '0', 'content:friend:add', '#', '', '2020-04-01 10:33:14', '2020-04-01 10:33:14');
INSERT INTO `sys_menu` VALUES (2013, '友链修改', 105, 3, '', NULL, 1, 'F', '0', 'content:friend:edit', '#', '', '2020-04-01 10:33:28', '2020-04-01 10:33:28');
INSERT INTO `sys_menu` VALUES (2014, '友链删除', 105, 4, '', NULL, 1, 'F', '0', 'content:friend:remove', '#', '', '2020-04-01 10:33:50', '2020-04-01 10:33:58');
INSERT INTO `sys_menu` VALUES (2015, '评论查询', 106, 1, '', NULL, 1, 'F', '0', 'content:comment:query', '#', '', '2020-04-01 10:34:43', '2020-04-01 10:34:43');
INSERT INTO `sys_menu` VALUES (2016, '评论删除（逻辑删除）', 106, 2, '', NULL, 1, 'F', '0', 'content:comment:delete', '#', '', '2020-04-01 10:35:21', '2020-04-01 10:35:21');
INSERT INTO `sys_menu` VALUES (2017, '日志查询', 107, 1, '', NULL, 1, 'F', '0', 'content:requestlog:query', '#', '', '2020-04-01 10:36:00', '2020-04-01 10:36:16');
INSERT INTO `sys_menu` VALUES (2018, '日志删除', 107, 2, '', NULL, 1, 'F', '0', 'content:requestlog:remove', '#', '', '2020-04-01 10:36:41', '2020-04-01 10:38:21');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称',
  `role_key` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色权限字符串',
  `role_sort` int(4) NOT NULL COMMENT '显示顺序',
  `data_scope` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '数据范围（1：全部数据权限 2：自定数据权限 3：本部门数据权限 4：本部门及以下数据权限）',
  `status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色状态（0正常 1停用）',
  `del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '删除标志（0代表存在 2代表删除）',
  `create_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '创建者',
  `update_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '更新者',
  `remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
  `create_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, '管理员', 'admin', 1, '1', '0', '0', 'admin', 'ry', '管理员', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_role` VALUES (2, '普通角色', 'common', 2, '2', '0', '0', 'admin', 'ry', '普通角色', '2018-03-16 11:33:00', '2018-03-16 11:33:00');

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu`  (
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  `menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES (2, 2);
INSERT INTO `sys_role_menu` VALUES (2, 102);
INSERT INTO `sys_role_menu` VALUES (2, 103);
INSERT INTO `sys_role_menu` VALUES (2, 104);
INSERT INTO `sys_role_menu` VALUES (2, 105);
INSERT INTO `sys_role_menu` VALUES (2, 106);
INSERT INTO `sys_role_menu` VALUES (2, 107);
INSERT INTO `sys_role_menu` VALUES (2, 108);
INSERT INTO `sys_role_menu` VALUES (2, 2003);
INSERT INTO `sys_role_menu` VALUES (2, 2007);
INSERT INTO `sys_role_menu` VALUES (2, 2011);
INSERT INTO `sys_role_menu` VALUES (2, 2015);
INSERT INTO `sys_role_menu` VALUES (2, 2017);

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `us_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '姓名',
  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '账号',
  `password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
  `email` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `about` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介',
  `location` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地区',
  `wechat` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '微信',
  `qq` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'QQ',
  `avatar` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
  `role_id` int(11) NOT NULL COMMENT '权限号',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  `oauth_us_id` int(11) NULL DEFAULT NULL COMMENT 'OAuth2用户ID',
  `oauth_token` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'OAuth2用户token',
  `oauth_type` int(11) NULL DEFAULT NULL COMMENT 'OAuth2类型',
  PRIMARY KEY (`us_id`) USING BTREE,
  UNIQUE INDEX `sys_user_UN`(`oauth_us_id`, `oauth_type`) USING BTREE,
  INDEX `role_id`(`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, '低调做个路人', 'admin', 'admin', '1143038749@qq.com', '他的希望和信心从不消失，如今正像微风渐起那么重新旺盛起来。—《老人与海》', '北京朝阳区', '18531159505', '1143038749', 'http://sjpeng.top/140b9dad72e74e8692dd94df5dd5a561.jpeg', 1, '2019-12-06 14:33:53', '2022-03-28 16:55:17', NULL, '', NULL);
INSERT INTO `sys_user` VALUES (2, '游客2', 'visitors', '123456', '123456789@qq.com', '第一个', '北京', '123456789', '123456789', 'http://sjpeng.top/fd1fd0d60e264f6b9f01ec996622441b.jpeg', 2, '2020-01-06 14:33:53', '2022-03-28 16:55:17', NULL, '', NULL);

-- ----------------------------
-- Table structure for t_blog
-- ----------------------------
DROP TABLE IF EXISTS `t_blog`;
CREATE TABLE `t_blog`  (
  `bl_id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '标题',
  `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '内容',
  `outline` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '简介',
  `background_image` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '背景图片',
  `recommend` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否推荐',
  `commentabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否可以评论',
  `published` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否发布',
  `views` int(11) NOT NULL DEFAULT 0 COMMENT '访问量',
  `ty_id` int(11) NOT NULL COMMENT '类型id',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`bl_id`) USING BTREE,
  INDEX `ty_id`(`ty_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 140 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_blog
-- ----------------------------
INSERT INTO `t_blog` VALUES (1, 'CAS', '<h5>CAS全称Compare-And-Swap,他是一条cpu并发原语.</h5>\n<h5>它的功能是判断内存某个位置是否是预期值,是的话更改为更新值,是一条原子操作.</h5>\n<h5>CAS并发原语在java语言中是sun.misc.Unsafe 类中的各个方法. 调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖硬件的功能,通过它实现了原子操作.</h5>\n<h5>由于CAS是一种系统原语,原语属于操作系统范畴,由若干条指令组成,用于完成某一功能的过程.</h5>\n<h5>并且原语的执行过程必须是连续的.在执行过程中无法被打断,所以不会造成数据不一致问题.</h5>\n<h4 id=\"UnSafe-类\">UnSafe 类</h4>\n<pre class=\"language-java\"><code>public class AtomicInteger extends Number implements java.io.Serializable {\n    private static final long serialVersionUID = 6214790243416807050L;\n\n    // setup to use Unsafe.compareAndSwapInt for updates\n    private static final Unsafe unsafe = Unsafe.getUnsafe();\n    private static final long valueOffset;\n\n    static {\n        try {\n            // 获取下面 value 的地址偏移量\n            valueOffset = unsafe.objectFieldOffset\n                (AtomicInteger.class.getDeclaredField(\"value\"));\n        } catch (Exception ex) { throw new Error(ex); }\n    }\n\n    private volatile int value;\n	// ...\n}</code></pre>\n<ul>\n<li>Unsafe 是 CAS 的核心类，由于 Java 方法无法直接访问底层系统，而需要通过本地（native）方法来访问， Unsafe 类相当一个后门，基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中，其内部方法操作可以像 C 指针一样直接操作内存，因为 Java 中 CAS 操作执行依赖于 Unsafe 类。</li>\n<li>变量 vauleOffset，表示该变量值在内存中的偏移量，因为 Unsafe 就是根据内存偏移量来获取数据的。</li>\n<li>变量 value 用 volatile 修饰，保证了多线程之间的内存可见性。</li>\n</ul>\n<p><strong>getAndAddInt 方法</strong></p>\n<pre class=\"language-java\"><code>// unsafe.getAndAddInt\npublic final int getAndAddInt(Object obj, long valueOffset, long expected, int val) {\n    int temp;\n    do {\n        temp = this.getIntVolatile(obj, valueOffset);  // 获取快照值\n    } while (!this.compareAndSwap(obj, valueOffset, temp, temp + val));  // 如果此时 temp 没有被修改，就能退出循环，否则重新获取\n    return temp;\n}</code></pre>\n<h3 id=\"CAS-的缺点？\">CAS 的缺点</h3>\n<ul>\n<li>循环时间长开销很大\n<ul>\n<li>如果 CAS 失败，会一直尝试，如果 CAS 长时间一直不成功，可能会给 CPU 带来很大的开销（比如线程数很多，每次比较都是失败，就会一直循环），所以希望是线程数比较小的场景。</li>\n</ul>\n</li>\n<li>只能保证一个共享变量的原子操作\n<ul>\n<li>对于多个共享变量操作时，循环 CAS 就无法保证操作的原子性。</li>\n</ul>\n</li>\n<li>引出 ABA 问题</li>\n</ul>\n<h5 style=\"padding-left: 40px;\">ABA 问题是怎么产生的？当有一个值从 A 改为 B 又改为 A，这就是 ABA 问题。<br /><br />解决方案：时间戳原子引用</h5>', 'CAS全称Compare-And-Swap,他是一条cpu并发原语.', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 580, 1, '2019-11-26 14:00:00', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (4, '反射中Class.forName和classloader区别', '<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;在java中Class.forName()和ClassLoader都可以对类进行加载。ClassLoader就是遵循<strong>双亲委派模型</strong>最终调用启动类加载器的类加载器，实现的功能是&ldquo;通过一个类的全限定名来获取描述此类的二进制字节流&rdquo;，获取到二进制流后放到JVM中。Class.forName()方法实际上也是调用的CLassLoader来实现的。</p>\n<p>&nbsp; &nbsp;&nbsp;&nbsp; Class.forName(String className)；这个方法的源码是</p>\n<pre class=\"language-java\"><code> @CallerSensitive\n    public static Class&lt;?&gt; forName(String className)\n                throws ClassNotFoundException {\n        Class&lt;?&gt; caller = Reflection.getCallerClass();\n        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);\n    }</code></pre>\n<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;最后调用的方法是forName0这个方法，在这个forName0方法中的第二个参数被默认设置为了true，这个参数代表是否对加载的类进行初始化，设置为true时会类进行初始化，代表会执行类中的静态代码块，以及对静态变量的赋值等操作。</p>\n<p>也可以调用Class.forName(String name, boolean initialize,ClassLoader loader)方法来手动选择在加载类的时候是否要对类进行初始化。Class.forName(String name, boolean initialize,ClassLoader loader)的源码如下：</p>\n<pre class=\"language-java\"><code> @CallerSensitive\n    public static Class&lt;?&gt; forName(String name, boolean initialize,\n                                   ClassLoader loader)\n        throws ClassNotFoundException\n    {\n        Class&lt;?&gt; caller = null;\n        SecurityManager sm = System.getSecurityManager();\n        if (sm != null) {\n            // Reflective call to get caller class is only needed if a security manager\n            // is present.  Avoid the overhead of making this call otherwise.\n            caller = Reflection.getCallerClass();\n            if (sun.misc.VM.isSystemDomainLoader(loader)) {\n                ClassLoader ccl = ClassLoader.getClassLoader(caller);\n                if (!sun.misc.VM.isSystemDomainLoader(ccl)) {\n                    sm.checkPermission(\n                        SecurityConstants.GET_CLASSLOADER_PERMISSION);\n                }\n            }\n        }\n        return forName0(name, initialize, loader, caller);\n    }</code></pre>\n<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;源码中的注释描述中，其中对参数initialize的描述是：if {@code true} the class will be initialized.意思就是说：如果参数为true，则加载的类将会被初始化。</p>\n<hr />\n<h1 class=\"title-article\">&nbsp;&nbsp;&nbsp;类加载过程</h1>\n<p>&nbsp;&nbsp; &nbsp;&nbsp; <strong>1：加载</strong>&nbsp;&nbsp;&nbsp;Jvm把class文件字节码加载到内存中，并将这些静态数据装换成运行时数据区中方法区的类型数据，在运行时数据区堆中生成一个代表这个类</p>\n<p><strong>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;2：链接</strong>：执行下面的校验、准备和解析步骤，其中解析步骤是可选的</p>\n<p>　　　　a：校验：检查加载的class文件的正确性和安全性</p>\n<p>　　　　b：准备：为类变量分配存储空间并设置类变量初始值，类变量随类型信息存放在方法区中,生命周期很长，使用不当和容易造成内存泄漏。</p>\n<p><em>　　　　*</em><em><strong>释</strong>：类变量就是</em><em>static</em><em>变量；初始值指的是类变量类型的默认值而不是实际要赋的值</em></p>\n<p>　　　　c：解析：jvm将常量池内的符号引用转换为直接引用</p>\n<p><strong>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;3：初始化</strong>：执行类变量赋值和静态代码块</p>\n<hr />\n<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;Class.forName得到的class是已经初始化完成的</p>\n<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;Classloder.loaderClass得到的class是还没有链接的</p>\n<hr />\n<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;有些情况是只需要知道这个类的存在而不需要初始化的情况使用Classloder.loaderClass，而有些时候又必须执行初始化就选择Class.forName</p>\n<p>　　例如：数据库驱动加载就是使用Class.froName(&ldquo;com.mysql.jdbc.Driver&rdquo;),</p>\n<hr />\n<p><a href=\"https://www.cnblogs.com/jimoer/p/9185662.html\" target=\"_blank\" rel=\"noopener\">原文：https://www.cnblogs.com/jimoer/p/9185662.html</a></p>', '为什么要把ClassLoader.loadClass(String name)和Class.forName(String name)进行比较呢，因为他们都能在运行时对任意一个类，都能够知道该类的所有属性和方法；对于任意一个对象，都能够调用它的任意方法和属性。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 1, 1, 1, 329, 5, '2019-11-26 14:00:00', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (5, '多线程中的虚假唤醒', '<div>\n<h1>什么是虚假唤醒？</h1>\n</div>\n<div>一般而言线程调用wait()方法后，需要其他线程调用notify,notifyAll方法后，线程才会从wait方法中返回， 而虚假唤醒(spurious wakeup)是指线程通过其他方式，从wait方法中返回。</div>\n<div>&nbsp;</div>\n<div>假设有两个消费者线程，两个生产者线程。</div>\n<div>先上错误代码，是用if的情况：</div>\n<div>\n<pre class=\"language-java\"><code>public class AirCondition {\n    private int num=0;\n\n    private synchronized void production() throws InterruptedException {\n        //1.判断\n        if(num&gt;=1){\n            this.wait();\n        }\n        //2.执行\n        this.num++;\n        System.out.println(Thread.currentThread().getName()+\"生产1个此时资源量：\"+num);\n        //3.通知\n        notifyAll();\n\n    }\n    private synchronized void consume() throws InterruptedException {\n        if(num&lt;=0){\n            this.wait();\n        }\n        this.num--;\n        System.out.println(Thread.currentThread().getName()+\"消费1个此时资源量：\"+num);\n        notifyAll();\n    }\n    \n    public static void main(String[] args) {\n        AirCondition airCondition=new AirCondition();\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.consume();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"消费者1\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.consume();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"消费者2\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.production();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"生产者1\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.production();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"生产者2\").start();\n    }\n\n}</code></pre>\n<pre class=\"language-java\"><code>生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n消费者1消费1个此时资源量：-1\n生产者2生产1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n消费者2消费1个此时资源量：-1\n生产者1生产1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n消费者1消费1个此时资源量：-1\n生产者2生产1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n消费者2消费1个此时资源量：-1\n生产者1生产1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n消费者1消费1个此时资源量：-1\n生产者2生产1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n消费者2消费1个此时资源量：-1\n生产者1生产1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n消费者1消费1个此时资源量：-1\n生产者2生产1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n消费者2消费1个此时资源量：-1\n生产者1生产1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n消费者1消费1个此时资源量：-1\n生产者2生产1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n消费者2消费1个此时资源量：-1\n生产者1生产1个此时资源量：0</code></pre>\n<p>发现执行时资源量出现了-1的情况原因：</p>\n</div>\n<ol>\n<li>消费者1号线程抢到锁，进入同步代码块，发现没有资源，消费者1进入wait等待状态，外面三个线程竞争锁。</li>\n<li>消费者2号线程抢到锁，进入同步代码块，发现没有资源，消费者2进入wait等待状态，外面两个线程竞争锁。（此时消费者1,2都处于等待状态）</li>\n<li>生产者1号线程抢到锁，进入同步代码块，发现没有资源，执行生产资源，然后notifyAll。（此时消费者1,2都被唤醒）</li>\n<li>消费者1从wait往后执行，不在进行if判断 消费了1个资源，资源剩余成为0。</li>\n<li>消费者2从wait往后执行，不在进行if判断 消费了1个资源，资源剩余成为-1。（bug产生）</li>\n</ol>\n<div>解决的办法是条件判断通过</div>\n<div><span style=\"color: #ff0000;\">while(条件){</span></div>\n<div><span style=\"color: #ff0000;\">&nbsp; this.wait();</span></div>\n<div><span style=\"color: #ff0000;\">}</span></div>\n<div>来解决</div>\n<div>使用while后的结果：</div>\n<div>\n<pre class=\"language-java\"><code>生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0</code></pre>\n</div>\n<div>此时能够正常运行了。</div>\n<div>&nbsp;</div>\n<div>贴一下正确的代码：\n<pre class=\"language-java\"><code>public class AirCondition {\n    private int num=0;\n\n    private synchronized void production() throws InterruptedException {\n        //1.判断\n        while(num&gt;=1){\n            this.wait();\n        }\n        //2.执行\n        this.num++;\n        System.out.println(Thread.currentThread().getName()+\"生产1个此时资源量：\"+num);\n        //3.通知\n        notifyAll();\n\n    }\n    private synchronized void consume() throws InterruptedException {\n        while(num&lt;=0){\n            this.wait();\n        }\n        this.num--;\n        System.out.println(Thread.currentThread().getName()+\"消费1个此时资源量：\"+num);\n        notifyAll();\n    }\n\n    public static void main(String[] args) {\n        AirCondition airCondition=new AirCondition();\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.consume();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"消费者1\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.consume();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"消费者2\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.production();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"生产者1\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.production();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"生产者2\").start();\n    }\n\n}\n​</code></pre>\n</div>\n<h1>使用while()判断的原因</h1>\n<div><span style=\"color: #ff0000;\">while即使在条件成立的时候也还会再执行一次，而if判断条件成立了，就直接向下执行了</span></div>\n<div>&nbsp;</div>\n<div>\n<div>&nbsp;</div>\n<div>wait方法可以分为三个操作：</div>\n<div>（1）释放锁并阻塞</div>\n<div>（2）等待条件cond发生</div>\n<div>（3）获取通知后，竞争获取锁</div>\n</div>\n<div>在多核处理器下，pthread_cond_signal可能会激活多于一个线程（阻塞在条件变量上的线程）。结果就是，当一个线程调用pthread_cond_signal()后，多个调用pthread_cond_wait()或pthread_cond_timedwait()的线程返回。这种效应就称为&ldquo;虚假唤醒&rdquo;。</div>', '多线程使用while()判断wait的原因', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 1, 1, 1, 328, 1, '2019-11-26 14:00:00', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (6, 'JMM（Java 内存模型）', '<h4 id=\"基本概念\">基本概念</h4>\n<ul>\n<li>JMM 本身是一种抽象的概念并不是真实存在，它描述的是一组规定或则规范，通过这组规范定义了程序中的访问方式。</li>\n<li>JMM 同步规定\n<ul>\n<li>线程解锁前，必须把共享变量的值刷新回主内存</li>\n<li>线程加锁前，必须读取主内存的最新值到自己的工作内存</li>\n<li>加锁解锁是同一把锁</li>\n</ul>\n</li>\n<li>由于 JVM 运行程序的实体是线程，而每个线程创建时 JVM 都会为其创建一个工作内存，工作内存是每个线程的私有数据区域，而 Java 内存模型中规定所有变量的储存在主内存，主内存是共享内存区域，所有的线程都可以访问，但线程对变量的操作（读取赋值等）必须都工作内存进行看。</li>\n<li>首先要将变量从主内存拷贝的自己的工作内存空间，然后对变量进行操作，操作完成后再将变量写回主内存，不能直接操作主内存中的变量，工作内存中存储着主内存中的变量副本拷贝，前面说过，工作内存是每个线程的私有数据区域，因此不同的线程间无法访问对方的工作内存，线程间的通信(传值)必须通过主内存来完成。</li>\n</ul>\n<p>内存模型图</p>\n<p><img src=\"http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20190416211412.png\" alt=\"\" /></p>', 'JMM 本身是一种抽象的概念并不是真实存在，它描述的是一组规定或则规范，通过这组规范定义了程序中的访问方式。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 325, 1, '2019-11-26 14:00:00', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (7, '对volatile的理解', '<div><span style=\"color: #ff0000;\">volatile:1 保证可见性 2 禁止指令重排 3 不保证原子性</span></div>\n<div>&nbsp;</div>\n<h5>以多核CPU为例（两核），我们知道CPU的速度比内存要快得多，为了弥补这个性能差异，CPU内核都会有自己的高速缓存区，当内核运行的线程执行一段代码时，首先将这段代码的指令集进行缓存行填充到高速缓存，如果非volatil变量当CPU执行修改了此变量之后，会将修改后的值回写到高速缓存，然后再刷新到内存中。如果在刷新会内存之前，由于是共享变量，那么CORE2中的线程执行的代码也用到了这个变量，这是变量的值依然是旧的。volatile关键字就会解决这个问题的，如何解决呢，首先被volatile关键字修饰的共享变量在转换成汇编语言时，会加上一个以lock为前缀的指令，当CPU发现这个指令时，立即做两件事：</h5>\n<h5>1.将当前内核高速缓存行的数据立刻回写到内存；</h5>\n<h5>2.使在其他内核里缓存了该内存地址的数据无效。</h5>\n<h5>第一步很好理解，第二步如何做到呢？</h5>\n<h5>MESI协议：在早期的CPU中，是通过在总线加LOCK#锁的方式实现的，但这种方式开销太大，所以Intel开发了缓存一致性协议，也就是MESI协议，该解决缓存一致性的思路是：当CPU写数据时，如果发现操作的变量是共享变量，即在其他CPU中也存在该变量的副本，那么他会发出信号通知其他CPU将该变量的缓存行设置为无效状态。当其他CPU使用这个变量时，首先会去嗅探是否有对该变量更改的信号，当发现这个变量的缓存行已经无效时，会从新从内存中读取这个变量。</h5>\n<h5>并发编程的三大概念</h5>\n<h5><strong>可见性</strong></h5>\n<h5>可见性是一种复杂的属性，因为可见性中的错误总是会违背我们的直觉。通常，我们无法确保执行读操作的线程能适时看到其他线程写入的值，有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性，必须使用同步机制。</h5>\n<h5>可见性，是指线程之间的可见性，一个线程修改的状态对另一个线程是可见的。也就是线程修改的结果。</h5>\n<h5>另一个线程马上就能看到。比如：用volattitle修饰的变量，就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序，即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题，volatile只能让他修饰内容具有可见性，但不能保证他具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性，但是a++ 依然是一个非原子操作，也就是这个操作同样存在线程安全问题。</h5>\n<h5>而普通的共享变量不能保证可见性，因为普通共享变量被修改之后，什么时候被写入主存是不确定的，当其他线程去读取时，此时内存中可能还是原来的旧值，因此无法保证可见性。</h5>\n<h5>在 Java 中通过synchronized和Lock也能够保证可见性，synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码，并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。</h5>\n<h5><strong>原子性</strong></h5>\n<h5>原子是世界上的最小单位，具有不可分割性。原子性：即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断，要么就都不执行。在Java中，对基本数据类型的变量的读取和赋值操作是原子性操作，即这些操作是不可被中断的，要么执行，要么不执行。</h5>\n<h5>比如 a=0;(a非long和double类型) 这个操作是不可分割的，那么我们说这个操作时原子操作。再比如：a++; 这个操作实际是a = a + 1;是可分割的，所以他不是一个原子操作。非原子操作都会存在线程安全问题，需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作，那么我们称它具有原子性。java的concurrent包下提供了一些原子类，我们可以通过阅读API来了解这些原子类的用法。比如：AtomicInteger、AtomicLong、AtomicReference等。</h5>\n<h5>Java内存模型只保证了基本读取和赋值是原子性操作，如果要实现更大范围操作的原子性，可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块，那么自然就不存在原子性问题了，从而保证了原子性。</h5>\n<h5><strong>有序性</strong></h5>\n<h5>有序性就是程序执行的顺序按照代码的先后顺序执行。</h5>\n<h5>什么是指令重排序，一般来说，处理器为了提高程序运行效率，可能会对输入代码进行优化，它不保证程序中各个语句的执行先后顺序同代码中的顺序一致，但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行，但是会影响到线程并发执行的正确性。也就是说，要想并发程序正确地执行，必须要保证原子性、可见性以及有序性。只要有一个没有被保证，就有可能会导致程序运行不正确。</h5>\n<h5>在Java内存模型中，允许编译器和处理器对指令进行重排序，但是重排序过程不会影响到单线程程序的执行，却会影响到多线程并发执行的正确性。</h5>\n<h5>在Java里面，可以通过volatile关键字来保证一定的&ldquo;有序性&rdquo;。另外可以通过synchronized和Lock来保证有序性，很显然，synchronized和Lock保证每个时刻是有一个线程执行同步代码，相当于是让线程顺序执行同步代码，自然就保证了有序性。</h5>\n<h5>另外，Java内存模型具备一些先天的&ldquo;有序性&rdquo;，即不需要通过任何手段就能够得到保证的有序性，这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来，那么它们就不能保证它们的有序性，虚拟机可以随意地对它们进行重排序</h5>\n<div><hr /></div>\n<div>\n<h4 id=\"禁止指令排序\">禁止指令排序</h4>\n<p>volatile 实现禁止指令重排序的优化，从而避免了多线程环境下程序出现乱序的现象</p>\n<p>先了解一个概念，内存屏障（Memory Barrier）又称内存栅栏，是一个 CPU 指令，他的作用有两个：</p>\n<ul>\n<li>保证特定操作的执行顺序</li>\n<li>保证某些变量的内存可见性（利用该特性实现 volatile 的内存可见性）</li>\n</ul>\n<p>由于编译器个处理器都能执行指令重排序优化，如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU，不管什么指令都不能个这条 Memory Barrier 指令重排序，也就是说通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据，因此任何 CPU 上的线程都能读取到这些数据的最新版本。</p>\n<p>下面是保守策略下，volatile写插入内存屏障后生成的指令序列示意图：</p>\n<p><img src=\"http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/0e75180bf35c40e2921493d0bf6bd684_th.png\" alt=\"\" /></p>\n<p>下面是在保守策略下，volatile读插入内存屏障后生成的指令序列示意图：</p>\n<p><img src=\"http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/21ebc7e8190c4966948c4ef4424088be_th.png\" alt=\"\" /></p>\n<h4 id=\"线程安全性保证\">线程安全性保证</h4>\n<ul>\n<li>工作内存与主内存同步延迟现象导致可见性问题\n<ul>\n<li>可以使用 synchronzied 或 volatile 关键字解决，它们可以使用一个线程修改后的变量立即对其他线程可见</li>\n</ul>\n</li>\n<li>对于指令重排导致可见性问题和有序性问题\n<ul>\n<li>可以利用 volatile 关键字解决，因为 volatile 的另一个作用就是禁止指令重排序优化</li>\n</ul>\n</li>\n</ul>\n</div>\n<div><hr />\n<h1 class=\"_1RuRku\">双重校验单例模式的经典实现，这里使用volatile防止指令重排</h1>\n<pre class=\"line-numbers  language-java\"><code>class Singleton{\n    private volatile static Singleton singleton;   \n    public static Singleton getInstance(){       \n\n        if(singleton == null){                      // 语句1\n            synchronized(Singleton.class){          // 语句2\n                if(singleton == null){              // 语句3\n                    singleton = new Singleton();    // 语句4\n                }\n            }\n        } \n        \n        return singleton;           \n    }\n}</code></pre>\n<pre class=\"line-numbers  language-tsx\"><code>new 一个对象实际是4个步骤：\na. 看class对象是否加载，如果没有就先加载class对象，\nb. 分配内存空间，初始化实例。\nc. 调用构造函数。\nd. 返回地址给引用。</code></pre>\n<div>\n<div>\n<h6>不加volatile会出现什么问题</h6>\n<ul>\n<li>两个线程A,B B执行到了语句4，A执行到了语句1</li>\n<li>B因为指令重排，c，d被颠倒了，恰好d执行完了,c还没执行的时候B被挂起了。</li>\n<li>此时A运行到了语句1， 发现<code>singleton</code>不等于null,于是将还没构造完成的singleton对象返回给了上层调用。</li>\n</ul>\n</div>\n</div>\n</div>', '1 保证可见性 2 禁止指令重排 3 不保证原子性', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 327, 1, '2019-11-26 14:00:00', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (8, 'JAVA垃圾回收-可达性分析算法', '<p>在java中是通过引用来和对象进行关联的，也就是说如果要操作对象，必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性，如果一个对象没有任何引用与之关联，则说明该对象基本不太可能在其他地方被使用到，那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。</p>\n<p>这种方式的特点是实现简单，而且效率较高，但是它无法解决循环引用的问题，因此在Java中并没有采用这种方式（Python采用的是引用计数法）。看下面这段代码：</p>\n<pre class=\"language-java\"><code>public class Main {\n    public static void main(String[] args) {\n        MyObject object1 = new MyObject();\n        MyObject object2 = new MyObject();\n \n        object1.object = object2;\n        object2.object = object1;\n \n        object1 = null;\n        object2 = null;\n    }\n}\n \nclass MyObject{\n    public Object object = null;\n}</code></pre>\n<p><span style=\"color: #ff0000;\">最后面两句将object1和object2赋值为null，也就是说object1和object2指向的对象已经不可能再被访问，但是由于它们互相引用对方，导致它们的引用计数都不为0，那么垃圾收集器就永远不会回收它们。</span></p>\n<p>&nbsp;</p>\n<p>为了解决这个问题，在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的&ldquo;GC Roots&rdquo;对象作为起点进行搜索，如果在&ldquo;GC Roots&rdquo;和一个对象之间没有可达路径，则称该对象是不可达的，不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程，如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性，则基本上就真的成为可回收对象了。最后面两句将object1和object2赋值为null，也就是说object1和object2指向的对象已经不可能再被访问，但是由于它们互相引用对方，导致它们的引用计数都不为0，那么垃圾收集器就永远不会回收它们。</p>\n<p>Java并不采用引用计数法来判断对象是否已&ldquo;死&rdquo;，而采用&ldquo;可达性分析&rdquo;来判断对象是否存活（同样采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言）。&nbsp;<br />此算法的核心思想：通过一系列称为&ldquo;GC Roots&rdquo;的对象作为起始点，从这些节点开始向下搜索，搜索走过的路径称为&ldquo;引用链&rdquo;，当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时，证明此对象不可用。以下图为例：</p>\n<p><img src=\"https://img-blog.csdn.net/20180626084654607?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3l1YnVqaWFuX2w=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70\" alt=\"\" width=\"714\" height=\"534\" /></p>\n<p>对象Object5 &mdash;Object7之间虽然彼此还有联系，但是它们到 GC Roots 是不可达的，因此它们会被判定为可回收对象。</p>\n<p>在Java语言中，可作为GC Roots的对象包含以下几种：</p>\n<ol>\n<li>虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)</li>\n<li>方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)</li>\n<li>方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)</li>\n<li>本地方法栈中(Native方法)引用的对象(可以理解为:<span style=\"color: #ff0000;\">引用Native方法的所有对象</span>)</li>\n</ol>\n<p>可以理解为:</p>\n<p>(1)<span style=\"color: #ff0000;\">首先第一种是虚拟机栈中的引用的对象，我们在程序中正常创建一个对象</span>，对象会在堆上开辟一块空间，同时会将这块空间的地址作为引用保存到虚拟机栈中，如果对象生命周期结束了，那么引用就会从虚拟机栈中出栈，因此如果在虚拟机栈中有引用，就说明这个对象还是有用的，这种情况是最常见的。</p>\n<p>(2)<span style=\"color: #ff0000;\">第二种是我们在类中定义了全局的静态的对象，也就是使用了static关键字</span>，由于虚拟机栈是线程私有的，所以这种对象的引用会保存在共有的方法区中，显然将方法区中的静态引用作为GC Roots是必须的。</p>\n<p>(3)<span style=\"color: #ff0000;\">第三种便是常量引用，就是使用了static final关键字</span>，由于这种引用初始化之后不会修改，所以方法区常量池里的引用的对象也应该作为GC Roots。最后一种是在使用JNI技术时，有时候单纯的Java代码并不能满足我们的需求，我们可能需要在Java中调用C或C++的代码，因此会使用native方法，JVM内存中专门有一块本地方法栈，用来保存这些对象的引用，所以本地方法栈中引用的对象也会被作为GC Roots。</p>\n<p><strong>JVM之判断对象是否存活（引用计数算法、可达性分析算法，最终判定）</strong></p>\n<p><strong>finalize()方法最终判定对象是否存活:</strong></p>\n<p>&nbsp;&nbsp;&nbsp; 即使在可达性分析算法中不可达的对象，也并非是&ldquo;非死不可&rdquo;的，这时候它们暂时处于&ldquo;缓刑&rdquo;阶段，要真正宣告一个对象死亡，至少要经历再次标记过程。<br />&nbsp;&nbsp;&nbsp; 标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。<br />&nbsp;&nbsp;1).第一次标记并进行一次筛选。<br />&nbsp;&nbsp;&nbsp; 筛选的条件是此对象是否有必要执行finalize()方法。<br />&nbsp;&nbsp;&nbsp; 当对象没有覆盖finalize方法，或者finzlize方法已经被虚拟机调用过，虚拟机将这两种情况都视为&ldquo;没有必要执行&rdquo;，对象被回收。<br /><br />&nbsp;&nbsp;2).第二次标记<br />&nbsp;&nbsp;&nbsp; 如果这个对象被判定为有必要执行finalize（）方法，那么这个对象将会被放置在一个名为：F-Queue的队列之中，并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的&ldquo;执行&rdquo;是指虚拟机会触发这个方法，但并不承诺会等待它运行结束。这样做的原因是，如果一个对象finalize（）方法中执行缓慢，或者发生死循环（更极端的情况），将很可能会导致F-Queue队列中的其他对象永久处于等待状态，甚至导致整个内存回收系统崩溃。<br />&nbsp;&nbsp;&nbsp; Finalize（）方法是对象脱逃死亡命运的最后一次机会，稍后GC将对F-Queue中的对象进行第二次小规模标记，如果对象要在finalize（）中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可，譬如把自己赋值给某个类变量或对象的成员变量，那在第二次标记时它将移除出&ldquo;即将回收&rdquo;的集合。如果对象这时候还没逃脱，那基本上它就真的被回收了。<br />流程图如下：</p>\n<p><img src=\"https://img-blog.csdn.net/20160514190019348\" alt=\"\" width=\"872\" height=\"341\" /></p>\n<p>在JDK1.2以前，Java中引用的定义很传统: 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址，就称这块内存代表着一个引用。这种定义有些狭隘，一个对象在这种定义下只有被引用或者没有被引用两种状态。&nbsp;<br />我们希望能描述这一类对象: 当内存空间还足够时，则能保存在内存中；如果内存空间在进行垃圾回收后还是非常紧张，则可以抛弃这些对象。很多系统中的缓存对象都符合这样的场景。&nbsp;<br />在JDK1.2之后，Java对引用的概念做了扩充，将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种，这四种引用的强度依次递减。</p>\n<p>⑴强引用（StrongReference）<br />强引用是使用最普遍的引用。如果一个对象具有强引用，那垃圾回收器绝不会回收它。当内存空间不足，Java虚拟机宁愿抛出OutOfMemoryError错误，使程序异常终止，也不会靠随意回收具有强引用的对象来解决内存不足的问题。&nbsp;&nbsp;ps：强引用其实也就是我们平时A a = new A()这个意思。<br /><br />⑵软引用（SoftReference）<br />如果一个对象只具有软引用，则内存空间足够，垃圾回收器就不会回收它；如果内存空间不足了，就会回收这些对象的内存。只要垃圾回收器没有回收它，该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存（下文给出示例）。<br />软引用可以和一个引用队列（ReferenceQueue）联合使用，如果软引用所引用的对象被垃圾回收器回收，Java虚拟机就会把这个软引用加入到与之关联的引用队列中。<br /><br />⑶弱引用（WeakReference）<br />弱引用与软引用的区别在于：只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中，一旦发现了只具有弱引用的对象，不管当前内存空间足够与否，都会回收它的内存。不过，由于垃圾回收器是一个优先级很低的线程，因此不一定会很快发现那些只具有弱引用的对象。<br />弱引用可以和一个引用队列（ReferenceQueue）联合使用，如果弱引用所引用的对象被垃圾回收，Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。<br /><br />⑷虚引用（PhantomReference）<br />&ldquo;虚引用&rdquo;顾名思义，就是形同虚设，与其他几种引用都不同，虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用，那么它就和没有任何引用一样，在任何时候都可能被垃圾回收器回收。<br />虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于：虚引用必须和引用队列 （ReferenceQueue）联合使用。当垃圾回收器准备回收一个对象时，如果发现它还有虚引用，就会在回收对象的内存之前，把这个虚引用加入到与之 关联的引用队列中。</p>\n<p>1 为什么需要使用软引用<br /><br />首先，我们看一个雇员信息查询系统的实例。我们将使用一个Java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档案信息。作为一个用户，我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样，我们在浏览WEB页面的时候也经常会使用&ldquo;后退&rdquo;按钮)。</p>\n<p>这时我们通常会有两种程序实现方式:</p>\n<p>一种是:</p>\n<p>把过去查看过的雇员信息保存在内存中，每一个存储了雇员档案信息的Java对象的生命周期贯穿整个应用程序始终;</p>\n<p>另一种是:</p>\n<p>当用户开始查看其他雇员的档案信息的时候，把存储了当前所查看的雇员档案信息的Java对象结束引用，使得垃圾收集线程可以回收其所占用的内存空间，当用户再次需要浏览该雇员的档案信息的时候，重新构建该雇员的信息。</p>\n<p>很显然，第一种实现方法将造成大量的内存浪费.</p>\n<p>而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集，包含雇员档案信息的对象仍然完好地保存在内存中，应用程序也要重新构建一个对象。</p>\n<p>我们知道，访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素，如果能重新获取那些尚未被回收的Java对象的引用，必将减少不必要的访问，大大提高程序的运行速度。</p>\n<p>&nbsp;</p>\n<div id=\"gtx-trans\" style=\"position: absolute; left: -11px; top: 2030px;\">\n<div class=\"gtx-trans-icon\">&nbsp;</div>\n</div>', '在java中是通过引用来和对象进行关联的，也就是说如果要操作对象，必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 284, 5, '2019-11-26 14:00:00', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (10, 'Reentrantlock ', '<pre><strong>ReentrantLock 即可重入锁，实现了 Lock 和 Serializable 接口。</strong><br /><strong>在 Java 环境下 ReentrantLock 和 synchronized 都是可重入锁。<br /></strong></pre>\n<div>Lock和synchronized有以下几点不同：</div>\n<ul>\n<li>\n<div>原始结构</div>\n</li>\n<ul>\n<li>\n<div>synchronized 是关键字属于 JVM 层面，反应在字节码上是 monitorenter 和 monitorexit，其底层是通过 monitor 对象来完成，其实 wait/notify 等方法也是依赖 monitor 对象只有在同步快或方法中才能调用 wait/notify 等方法。</div>\n</li>\n<li>\n<div>Lock 是具体类（java.util.concurrent.locks.Lock）是 api 层面的锁。</div>\n</li>\n</ul>\n<li>\n<div>使用方法</div>\n</li>\n<ul>\n<li>\n<div>synchronized 不需要用户手动去释放锁，当 synchronized 代码执行完后系统会自动让线程释放对锁的占用。</div>\n</li>\n<li>\n<div>ReentrantLock 则需要用户手动的释放锁，若没有主动释放锁，可能导致出现死锁的现象，lock() 和 unlock() 方法需要配合 try/finally 语句来完成。</div>\n</li>\n</ul>\n<li>\n<div>等待是否可中断</div>\n</li>\n<ul>\n<li>\n<div>synchronized 不可中断，除非抛出异常或者正常运行完成。</div>\n</li>\n<li>\n<div>ReentrantLock 可中断，设置超时方法 tryLock(long timeout, TimeUnit unit)，lockInterruptibly() 放代码块中，调用 interrupt() 方法可中断。</div>\n</li>\n</ul>\n<li>\n<div>加锁是否公平</div>\n</li>\n<ul>\n<li>\n<div>synchronized 非公平锁</div>\n</li>\n<li>\n<div>ReentrantLock 默认非公平锁，构造方法中可以传入 boolean 值，true 为公平锁，false 为非公平锁。</div>\n</li>\n</ul>\n<li>\n<div>锁可以绑定多个 Condition</div>\n</li>\n<ul>\n<li>\n<div>synchronized 没有 Condition。</div>\n</li>\n<li>\n<div>ReentrantLock 用来实现分组唤醒需要唤醒的线程们，可以精确唤醒，而不是像 synchronized 要么随机唤醒一个线程要么唤醒全部线程。</div>\n</li>\n</ul>\n</ul>\n<p>一个Reentrantlock版本的生产者消费者</p>\n<pre class=\"language-java\"><code>import java.util.concurrent.locks.Condition;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\npublic class AirCondition {\n    private int num = 0;\n    private Lock lock = new ReentrantLock();\n    private Condition condition = lock.newCondition();\n\n    private void production() {\n        lock.lock();\n        try {\n            //1.判断\n            while (num &gt;= 1) {\n                condition.await();//代替this.wait()\n            }\n            //2.执行\n            this.num++;\n            System.out.println(Thread.currentThread().getName() + \"生产1个此时资源量：\" + num);\n            //3.通知\n            condition.signalAll();//代替notifyAll()\n\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n\n\n    }\n\n    private void consume() {\n        lock.lock();\n        try {\n            while (num &lt;= 0) {\n                condition.await();\n            }\n            this.num--;\n            System.out.println(Thread.currentThread().getName() + \"消费1个此时资源量：\" + num);\n            condition.signalAll();\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n\n\n    }\n\n    public static void main(String[] args) {\n        AirCondition airCondition = new AirCondition();\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n              airCondition.consume();\n            }\n\n        }, \"消费者1\").start();\n\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n                airCondition.consume();\n            }\n\n        }, \"消费者2\").start();\n\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n                airCondition.production();\n            }\n\n        }, \"生产者1\").start();\n\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n                airCondition.production();\n            }\n\n        }, \"生产者2\").start();\n    }\n\n}\n</code></pre>\n<p>多线程之间按顺序调用实现 p1-&gt;p2-&gt;p3</p>\n<p>三个线程启动，p1 输出111 p2输出222 p3输出333 来十轮，如下</p>\n<pre class=\"language-java\"><code>import java.util.concurrent.locks.Condition;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\npublic class AirCondition {\n    private int flag = 1;\n    private Lock lock = new ReentrantLock();\n    private Condition c1 = lock.newCondition();//print1\n    private Condition c2 = lock.newCondition();//print2\n    private Condition c3 = lock.newCondition();//print3\n\n    private void print1() {\n        lock.lock();\n        try {\n            //1.判断\n            while (flag != 1) {\n                c1.await();//代替this.wait()\n            }\n            //2.执行\n            System.out.println(Thread.currentThread().getName()+\" 111\");\n            //3.通知下一个\n            flag=2;\n            c2.signal();\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    private void print2() {\n        lock.lock();\n        try {\n            //1.判断\n            while (flag != 2) {\n                c2.await();//代替this.wait()\n            }\n            //2.执行\n            System.out.println(Thread.currentThread().getName()+\" 222\");\n            //3.通知下一个\n            flag=3;\n            c3.signal();\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    private void print3() {\n        lock.lock();\n        try {\n            //1.判断\n            while (flag != 3) {\n                c3.await();//代替this.wait()\n            }\n            //2.执行\n            System.out.println(Thread.currentThread().getName()+\" 333\");\n            //3.通知下一个\n            flag=1;\n            c1.signal();\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n    }\n\n\n\n    public static void main(String[] args) {\n        AirCondition airCondition = new AirCondition();\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n              airCondition.print1();\n            }\n\n        }, \"p1\").start();\n\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n                airCondition.print2();\n            }\n\n        }, \"p2\").start();\n\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n                airCondition.print3();\n            }\n\n        }, \"p3\").start();\n\n\n    }   </code></pre>\n<pre class=\"language-java\"><code>p1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333</code></pre>', 'ReentrantLock 即可重入锁，实现了 Lock 和 Serializable 接口。在 Java 环境下 ReentrantLock 和 synchronized 都是可重入锁。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 1, 1, 1, 298, 1, '2019-11-26 14:00:00', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (11, '对 ThreadPoolExector 的理解', '<h3>为什使用线程池，线程池的优势？</h3>\n<div>线程池用于多线程处理中，它可以根据系统的情况，可以有效控制线程执行的数量，优化运行效果。线程池做的工作主要是控制运行的线程的数量，处理过程中将任务放入队列，然后在线程创建后启动这些任务，如果线程数量超过了最大数量，那么超出数量的线程排队等候，等其它线程执行完毕，再从队列中取出任务来执行。</div>\n<div>主要特点为：</div>\n<ul>\n<li>\n<div>线程复用</div>\n</li>\n<li>\n<div>控制最大并发数量</div>\n</li>\n<li>\n<div>管理线程</div>\n</li>\n</ul>\n<div>主要优点</div>\n<ul>\n<li>\n<div>降低资源消耗，通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。</div>\n</li>\n<li>\n<div>提高相应速度，当任务到达时，任务可以不需要的等到线程创建就能立即执行。</div>\n</li>\n<li>\n<div>提高线程的可管理性，线程是稀缺资源，如果无限制的创建，不仅仅会消耗系统资源，还会降低体统的稳定性，使用线程可以进行统一分配，调优和监控。</div>\n</li>\n</ul>\n<h3>创建线程的几种方式</h3>\n<ul>\n<li>\n<div>继承 Thread</div>\n</li>\n<li>\n<div>实现 Runnable 接口</div>\n</li>\n<li>\n<div>实现 Callable</div>\n</li>\n<li>使用线程池</li>\n</ul>\n<h4>编码实现</h4>\n<ul>\n<li>\n<div>Executors.newSingleThreadExecutor()：只有一个线程的线程池，因此所有提交的任务是顺序执行</div>\n</li>\n<li>\n<div>Executors.newCachedThreadPool()：线程池里有很多线程需要同时执行，老的可用线程将被新的任务触发重新执行，如果线程超过60秒内没执行，那么将被终止并从池中删除</div>\n</li>\n<li>\n<div>Executors.newFixedThreadPool()：拥有固定线程数的线程池，如果没有任务执行，那么线程会一直等待</div>\n</li>\n<li>\n<div>Executors.newScheduledThreadPool()：用来调度即将执行的任务的线程池</div>\n</li>\n<li>\n<div>Executors.newWorkStealingPool()： newWorkStealingPool适合使用在很耗时的操作，但是newWorkStealingPool不是ThreadPoolExecutor的扩展，它是新的线程池类ForkJoinPool的扩展，但是都是在统一的一个Executors类中实现，由于能够合理的使用CPU进行对任务操作（并行操作），所以适合使用在很耗时的任务中</div>\n</li>\n</ul>\n<h4>ThreadPoolExecutor</h4>\n<div>ThreadPoolExecutor作为java.util.concurrent包对外提供基础实现，以内部线程池的形式对外提供管理任务执行，线程调度，线程池管理等等服务。</div>\n<h3>线程池的几个重要参数介绍？</h3>\n<ol>\n<li>corePoolSize：线程池的核心线程数</li>\n<li>maximumPoolSize：线程池的最大线程数，此值必须大于等于1</li>\n<li>keepAliveTime：多余的空闲线程的存活时间，线程池数量超过corePoolSize，空闲时间达到keepAliveTime值时，多余空闲线程会被销毁直到剩下corePoolSize个线程为止</li>\n<li>TimeUnit：keepAliveTime的单位</li>\n<li>workQueue：任务队列，被提交但是尚未被执行的任务</li>\n<li>threadFactory：设置创建线程的工厂</li>\n<li class=\"p1\">RejectedExecutionHandler：拒绝策略，当提交任务数超过maxmumPoolSize+workQueue之和时，任务会交给RejectedExecutionHandler 来处理</li>\n</ol>\n<div><br />重点讲解： 其中比较容易让人误解的是：corePoolSize，maximumPoolSize，workQueue之间关系。说说线程池的底层工作原理？</div>\n<ol>\n<li>\n<div>当线程池小于corePoolSize时，新提交任务将创建一个新线程执行任务，即使此时线程池中存在空闲线程。</div>\n</li>\n<li>\n<div>当线程池达到corePoolSize时，新提交任务将被放入 workQueue 中，等待线程池中任务调度执行。</div>\n</li>\n<li>\n<div>当workQueue已满，且 maximumPoolSize 大于 corePoolSize 时，新提交任务会创建新线程执行任务。</div>\n</li>\n<li>\n<div>当提交任务数超过 maximumPoolSize 时，新提交任务由 RejectedExecutionHandler 处理。</div>\n</li>\n<li>\n<div>当线程池中超过corePoolSize 线程，空闲时间达到 keepAliveTime 时，关闭空闲线程 。</div>\n</li>\n<li>\n<div>当设置allowCoreThreadTimeOut(true) 时，线程池中 corePoolSize 线程空闲时间达到 keepAliveTime 也将关闭。</div>\n</li>\n</ol>\n<p><img src=\"http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/92ad4409-2ab4-388b-9fb1-9fc4e0d832cd.jpg\" alt=\"\" /></p>\n<h3 id=\"线程池的拒绝策略你谈谈？\">线程池的拒绝策略你谈谈？</h3>\n<ul>\n<li>是什么\n<ul>\n<li>等待队列已经满了，再也塞不下新的任务，同时线程池中的线程数达到了最大线程数，无法继续为新任务服务。</li>\n</ul>\n</li>\n<li>拒绝策略\n<ul>\n<li>AbortPolicy（默认）：直接抛出现RejectedExecutionException异常。</li>\n<li>CallerRunsPolicy：该策略既不会抛弃任务，也不会抛出异常，而是将某些任务回退到调用者，从而降低新任务的流量</li>\n<li>DiscardOldestPolicy：丢弃队列里等待最久的任务，然后把当前任务加入队列中尝试再次提交当前任务。</li>\n<li>DiscardPolicy：直接丢弃掉，不予任何处理也不抛出异常。</li>\n</ul>\n</li>\n</ul>\n<h3 id=\"你在工作中单一的、固定数的和可变的三种创建线程池的方法，你用哪个多，超级大坑？\">你在工作中单一的、固定数的和可变的三种创建线程池的方法，你用哪个多，超级大坑？</h3>\n<p>如果读者对Java中的阻塞队列有所了解的话，看到这里或许就能够明白原因了。</p>\n<p>Java中的BlockingQueue主要有两种实现，分别是ArrayBlockingQueue 和 LinkedBlockingQueue。</p>\n<p>ArrayBlockingQueue是一个用数组实现的有界阻塞队列，必须设置容量。</p>\n<p>LinkedBlockingQueue是一个用链表实现的有界阻塞队列，容量可以选择进行设置，不设置的话，将是一个无边界的阻塞队列，最大长度为Integer.MAX_VALUE。</p>\n<p>这里的问题就出在：不设置的话，将是一个无边界的阻塞队列，最大长度为Integer.MAX_VALUE。也就是说，如果我们不设置LinkedBlockingQueue的容量的话，其默认容量将会是Integer.MAX_VALUE。</p>\n<p>而newFixedThreadPool中创建LinkedBlockingQueue时，并未指定容量。此时，LinkedBlockingQueue就是一个无边界队列，对于一个无边界队列来说，是可以不断的向队列中加入任务的，这种情况下就有可能因为任务过多而导致内存溢出问题。</p>\n<p>上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上，并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了，这两种方式创建的最大线程数可能是Integer.MAX_VALUE，而创建这么多线程，必然就有可能导致OOM。</p>\n<h3 id=\"你在工作中是如何使用线程池的，是否自定义过线程池使用？\">你在工作中是如何使用线程池的，是否自定义过线程池使用？</h3>\n<p>自定义线程池</p>\n<pre class=\"language-java\"><code>public class ThreadPoolExecutorDemo {\n\n    public static void main(String[] args) {\n        Executor executor = new ThreadPoolExecutor(2, 3, 1L, TimeUnit.SECONDS,\n                new LinkedBlockingQueue&lt;&gt;(5), \n                Executors.defaultThreadFactory(), \n                new ThreadPoolExecutor.DiscardPolicy());\n    }\n}</code></pre>\n<h3 id=\"合理配置线程池你是如果考虑的？\">合理配置线程池你是如果考虑的？</h3>\n<ul>\n<li>CPU 密集型\n<ul>\n<li>CPU 密集的意思是该任务需要大量的运算，而没有阻塞，CPU 一直全速运行。</li>\n<li>CPU 密集型任务尽可能的少的线程数量，一般为 CPU 核数 + 1 个线程的线程池。</li>\n</ul>\n</li>\n<li>IO 密集型\n<ul>\n<li>由于 IO 密集型任务线程并不是一直在执行任务，可以多分配一点线程数，如 CPU * 2 。</li>\n<li>也可以使用公式：CPU 核数 / (1 - 阻塞系数)；其中阻塞系数在 0.8 ～ 0.9 之间。</li>\n</ul>\n</li>\n</ul>', '线程池用于多线程处理中，它可以根据系统的情况，可以有效控制线程执行的数量，优化运行效果。线程池做的工作主要是控制运行的线程的数量，处理过程中将任务放入队列，然后在线程创建后启动这些任务，如果线程数量超过了最大数量，那么超出数量的线程排队等候，等其它线程执行完毕，再从队列中取出任务来执行。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 1, 1, 1, 305, 1, '2019-11-26 14:00:00', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (12, 'CountDownLatch', '<div>\n<p>countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。</p>\n<p>是通过一个计数器来实现的，计数器的初始值是线程的数量。每当一个线程执行完毕后，计数器的值就-1，当计数器的值为0时，表示所有线程都执行完毕，然后在闭锁上等待的线程就可以恢复工作了。</p>\n<p>countDownLatch类中只提供了一个构造器：</p>\n<pre class=\"line-numbers  language-cpp\"><code>//参数count为计数值\npublic CountDownLatch(int count) {  };</code></pre>\n<pre class=\"line-numbers  language-java\"><code>//调用await()方法的线程会被挂起，它会等待直到count值为0才继续执行\npublic void await() throws InterruptedException { };   \n//和await()类似，只不过等待一定的时间后count值还没变为0的话就会继续执行\npublic boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  \n//将count值减1\npublic void countDown() { };  </code></pre>\n</div>\n<p>演示：教师有6个同学和班长，班长需要等6个同学都走后才能锁门。</p>\n<pre class=\"language-java\"><code>import java.util.concurrent.CountDownLatch;\n\npublic class CountdownLatchDemo {\n    public static void main(String[] args) throws InterruptedException {\n        CountDownLatch countDownLatch=new CountDownLatch(6);//6个线程\n        for (int i=0;i&lt;6;i++){\n            new Thread(()-&gt;{\n                System.out.println(Thread.currentThread().getName()+ \"学生离开教室\");\n                countDownLatch.countDown();\n            },String.valueOf(i)).start();\n        }\n        countDownLatch.await();//6线程个执行完才执行下面\n        System.out.println(\"班长锁门！\");\n\n    }\n}\n</code></pre>\n<pre class=\"language-java\"><code>0学生离开教室\n1学生离开教室\n3学生离开教室\n4学生离开教室\n2学生离开教室\n5学生离开教室\n班长锁门！</code></pre>\n<hr />\n<p><strong>场景说明：</strong></p>\n<ul>\n<li>模拟多线程分组计算</li>\n<li>有一个大小为50000的随机数组，用5个线程分别计算10000个元素的和</li>\n<li>然后在将计算结果进行合并，得出最后的结果。</li>\n</ul>\n<pre class=\"language-java\"><code>import java.util.Arrays;\nimport java.util.Random;\nimport java.util.concurrent.CountDownLatch;\n\npublic class CountdownLatchDemo {\n    public static void main(String[] args) throws InterruptedException {\n        //数组大小\n        int size = 50000;\n        //定义数组\n        int[] numbers = new int[size];\n        //随机初始化数组\n        Random random = new Random();\n        for (int i = 0; i &lt; size; i++) {\n            numbers[i] = random.nextInt(100);\n        }\n\n        //单线程计算结果\n        System.out.println();\n        Long sum = 0L;\n        for (int i = 0; i &lt; size; i++) {\n            sum += numbers[i];\n        }\n        System.out.println(\"单线程计算结果：\" + sum);\n//----------------------------------------------------------------------------------------------------------------------\n        //定义五个Future去保存子数组计算结果\n        final int[] results = new int[5];\n        //子数组长度\n        int length = 10000;\n        CountDownLatch countDownLatch=new CountDownLatch(5);//5个线程\n        for (int i=0;i&lt;5;i++){\n            int finalI = i;\n            int[] subNumbers = Arrays.copyOfRange(numbers, (i * length), ((i + 1) * length)); //每个线程计算一份数组\n            new Thread(()-&gt;{\n                for (int j = 0; j &lt;subNumbers.length ; j++) {\n                    results[finalI]+=subNumbers[j];\n                }\n                countDownLatch.countDown();\n            },String.valueOf(i)).start();\n        }\n        countDownLatch.await();//5个线程个执行完才执行下面\n        int sums = 0;\n        for (int i = 0; i &lt; 5; i++) {\n            sums += results[i];\n        }\n        System.out.println(\"多线程计算结果：\" + sums);\n\n    }\n}\n</code></pre>\n<pre class=\"language-java\"><code>单线程计算结果：2483567\n多线程计算结果：2483567</code></pre>', 'countDownLatch是在java1.5被引入，跟它一起被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 1, 1, 1, 275, 1, '2019-11-26 14:00:00', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (13, 'CyclicBarrier', '<p>CyclicBarrier，是JDK1.5的java.util.concurrent并发包中提供的一个并发工具类。</p>\n<p>所谓Cyclic即 循环 的意思，所谓Barrier即 屏障 的意思。</p>\n<p>所以综合起来，CyclicBarrier指的就是 循环屏障，虽然这个叫法很奇怪，但是确能很好地表示它的作用。<br /><br /></p>\n<hr />\n<h3 id=\"2cyclicbarrier方法说明\">CyclicBarrier方法说明</h3>\n<h4><span style=\"background-color: #00ccff;\">CyclicBarrier(parties,Runnable barrierAction)</span></h4>\n<h5>初始化相互等待的线程数量以及屏障线程的构造方法。屏障线程的运行时机：等待的线程数量=parties之后，CyclicBarrier打开屏障之前。</h5>\n<h4><span style=\"background-color: #00ccff;\">await()</span></h4>\n<h5>在CyclicBarrier上进行阻塞等待，直到发生以下情形之一：</h5>\n<ul>\n<li>\n<h5>在CyclicBarrier上等待的线程数量达到parties，则所有线程被释放，继续执行。</h5>\n</li>\n<li>\n<h5>当前线程被中断，则抛出InterruptedException异常，并停止等待，继续执行。</h5>\n</li>\n<li>\n<h5>当前线程被中断，则抛出InterruptedException异常，并停止等待，继续执行。</h5>\n</li>\n<li>\n<h5>其他等待的线程被中断，则当前线程抛出BrokenBarrierException异常，并停止等待，继续执行。</h5>\n</li>\n<li>\n<h5>其他线程调用CyclicBarrier.reset()方法，则当前线程抛出BrokenBarrierException异常，并停止等待，继续执行。</h5>\n</li>\n</ul>\n<p><span style=\"background-color: #00ccff;\"><strong>await(timeout,TimeUnit)</strong></span></p>\n<h5>在CyclicBarrier上进行限时的阻塞等待</h5>\n<hr />\n<h5>应用场景：收集齐7个龙珠可以召唤神龙，召唤后，还可以收集召唤。。。</h5>\n<pre class=\"language-java\"><code>import java.util.concurrent.BrokenBarrierException;\nimport java.util.concurrent.CyclicBarrier;\n\npublic class CyclicBarrierDemo {\n    public static void main(String[] args) {\n        CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()-&gt;{System.out.println(\"召唤神龙！\");});\n        for (int i = 0; i &lt;14 ; i++) {\n            final int tem=i;\n            new Thread(()-&gt;{\n                System.out.println(Thread.currentThread().getName()+\"收集到第\"+tem+\"个龙珠\");\n                try {\n                    cyclicBarrier.await();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                } catch (BrokenBarrierException e) {\n                    e.printStackTrace();\n                }\n            },String.valueOf(i)).start();\n        }\n    }\n}\n</code></pre>\n<pre class=\"language-java\"><code>0收集到第0个龙珠\n1收集到第1个龙珠\n2收集到第2个龙珠\n3收集到第3个龙珠\n4收集到第4个龙珠\n5收集到第5个龙珠\n6收集到第6个龙珠\n召唤神龙！\n7收集到第7个龙珠\n8收集到第8个龙珠\n11收集到第11个龙珠\n12收集到第12个龙珠\n9收集到第9个龙珠\n10收集到第10个龙珠\n13收集到第13个龙珠\n召唤神龙！</code></pre>\n<p>&nbsp;</p>', 'CyclicBarrier，是JDK1.5的java.util.concurrent并发包中提供的一个并发工具类。所谓Cyclic即 循环 的意思，所谓Barrier即 屏障 的意思。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 294, 1, '2019-11-26 14:00:00', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (43, 'Semaphore', '<p>Semaphore和ReentrantLock类似，获取许可有公平策略和非公平许可策略，默认情况下使用非公平策略。</p>\n<p>　　当初始值为1时，可以用作互斥锁，并具备不可重入的加锁语义。</p>\n<p>　　Semaphore将AQS的同步状态用于保存当前可用许可的数量。</p>\n<p><strong>实现资源池：</strong></p>\n<p>　　一个固定长度的资源池，当池为空时，请求资源会失败。</p>\n<p>　　使用Semaphore可以实现当池为空时，请求会阻塞，非空时解除阻塞。</p>\n<p>　　也可以使用Semaphore将任何一种容器变成有界阻塞容器。</p>\n<p>应用场景：</p>\n<p>以一个停车场是运作为例。假设停车场只有三个车位，一开始三个车位都是空的。这时如果同时来了六辆车，看门人允许其中三辆不受阻碍的进入，然后放下车拦，剩下的车则必须在入口等待，此后来的车也都不得不在入口处等待。这时，有一辆车离开停车场，看门人得知后，打开车拦，放入一辆，如果又离开两辆，则又可以放入两辆，如此往复。</p>\n<p>这个停车系统中，每辆车就好比一个线程，看门人就好比一个信号量，看门人限制了可以活动的线程。假如里面依然是三个车位，但是看门人改变了规则，要求每次只能停两辆车，那么一开始进入两辆车，后面得等到有车离开才能有车进入，但是得保证最多停两辆车。对于Semaphore类而言，就如同一个看门人，限制了可活动的线程数。<br /><br /></p>\n<pre class=\"language-java\"><code>import java.util.concurrent.Semaphore;\n\npublic class SemaphoreDemo {\n    public static void main(String[] args) {\n        Semaphore semaphore=new Semaphore(3);//最多3个车位\n        for (int i = 0; i &lt; 6; i++) {//6辆车\n            new Thread(()-&gt;{\n                try {\n                    semaphore.acquire();//占用车位\n                    System.out.println(Thread.currentThread().getName()+\" 抢到车位。\");\n                    Thread.sleep(2000);\n                    System.out.println(Thread.currentThread().getName()+\" 离开车位。\");\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }finally {\n                    semaphore.release();//释放信车位\n                }\n            },String.valueOf(i)).start();\n        }\n    }\n}\n</code></pre>\n<pre class=\"language-java\"><code>1 抢到车位。\n2 抢到车位。\n3 抢到车位。\n\n2 离开车位。\n3 离开车位。\n1 离开车位。\n5 抢到车位。\n0 抢到车位。\n4 抢到车位。\n\n0 离开车位。\n5 离开车位。\n4 离开车位。</code></pre>', 'Semaphore也叫信号量，在JDK1.5被引入，用来控制同时访问某个特定资源的操作数量，或者同时执行某个指定操作的数量。还可以用来实现某种资源池，或者对容器施加边界。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 276, 1, '2019-11-30 19:48:23', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (44, 'BlockingQueue', '<p>BlockingQueue即阻塞队列，它是基于ReentrantLock，依据它的基本原理，我们可以实现Web中的长连接聊天功能，当然其最常用的还是用于实现生产者与消费者模式，大致如下图所示：</p>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/4180398-89f0d2693361656e.png?imageMogr2/auto-orient/strip|imageView2/2/w/873/format/webp\" alt=\"\" /></p>\n<p>概念：</p>\n<ul>\n<li>当队列是空的，试图获取元素将阻塞</li>\n<li>当队列是满的，试图添加元素将阻塞</li>\n</ul>\n<p>为什么需要阻塞队列：</p>\n<p>我们不需要关心什么时候阻塞线程，一旦条件满足，被挂起的线程会自动唤醒。</p>\n<p>主要实现类：</p>\n<ul>\n<li>ArrayBlockingQueue：由数组组成的有界队列</li>\n<li>LinkedBlockingQueue：由链表组成的有界队列（默认大小是Integer.MAX&mdash;VALUE）</li>\n<li>SynchronousQueue：不存储元素的阻塞队列，即<span style=\"color: #ff0000;\">单个元素</span>的队列</li>\n<li>LinkedBlockingDeque：由链表组成的<span style=\"color: #ff0000;\">双向</span>阻塞队列。</li>\n</ul>\n<hr />\n<p>核心方法：</p>\n<table style=\"border-collapse: collapse; width: 53.1522%;\" border=\"1\">\n<tbody>\n<tr>\n<td style=\"width: 12.5%; text-align: center;\">方法类型</td>\n<td style=\"width: 12.5%; text-align: center;\">抛出异常</td>\n<td style=\"width: 12.5%; text-align: center;\">特殊值</td>\n<td style=\"width: 12.5%; text-align: center;\">阻塞</td>\n<td style=\"width: 12.5%; text-align: center;\">超时</td>\n</tr>\n<tr>\n<td style=\"width: 12.5%; text-align: center;\">插入</td>\n<td style=\"width: 12.5%; text-align: center;\">add（e）</td>\n<td style=\"width: 12.5%; text-align: center;\">offer（e）</td>\n<td style=\"width: 12.5%; text-align: center;\">put（e）</td>\n<td style=\"width: 12.5%; text-align: center;\">offer（e，time，unit）</td>\n</tr>\n<tr>\n<td style=\"width: 12.5%; text-align: center;\">移除</td>\n<td style=\"width: 12.5%; text-align: center;\">remove（）</td>\n<td style=\"width: 12.5%; text-align: center;\">poll（）</td>\n<td style=\"width: 12.5%; text-align: center;\">take（）</td>\n<td style=\"width: 12.5%; text-align: center;\">poll（e，unit）</td>\n</tr>\n<tr>\n<td style=\"width: 12.5%; text-align: center;\">检查</td>\n<td style=\"width: 12.5%; text-align: center;\">element（）</td>\n<td style=\"width: 12.5%; text-align: center;\">peek（）</td>\n<td style=\"width: 12.5%; text-align: center;\">不可用</td>\n<td style=\"width: 12.5%; text-align: center;\">不可用</td>\n</tr>\n</tbody>\n</table>\n<p>&nbsp;</p>\n<table style=\"border-collapse: collapse; width: 100%;\" border=\"1\">\n<tbody>\n<tr>\n<td style=\"width: 50%; text-align: center;\">抛出异常</td>\n<td style=\"width: 50%; text-align: center;\">\n<p>阻塞队列满，执行add抛出异常：IllegalStateException: Queue full</p>\n<p>阻塞队列空，remove抛出异常：NoSuchElementException</p>\n</td>\n</tr>\n<tr>\n<td style=\"width: 50%; text-align: center;\">特殊值</td>\n<td style=\"width: 50%; text-align: center;\">\n<p>插入 成功true，失败false</p>\n<p>移除 成功返回出队列的元素，失败返回null</p>\n</td>\n</tr>\n<tr>\n<td style=\"width: 50%; text-align: center;\">一直阻塞</td>\n<td style=\"width: 50%; text-align: center;\">\n<p>阻塞度列满时，生产者继续put，队列一直阻塞线程知道put or 响应中断退出</p>\n<p>阻塞队列空时，消费者take元素，队列会一直阻塞消费线程直到队列有数据</p>\n</td>\n</tr>\n<tr>\n<td style=\"width: 50%; text-align: center;\">超时退出</td>\n<td style=\"width: 50%; text-align: center;\">阻塞队列满时，队列阻塞生产者一段时间，超市后生产者线程退出</td>\n</tr>\n</tbody>\n</table>\n<p>&nbsp;</p>', 'BlockingQueue即阻塞队列，它是基于ReentrantLock，依据它的基本原理，我们可以实现Web中的长连接聊天功能，当然其最常用的还是用于实现生产者与消费者模式', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 297, 1, '2019-12-02 21:43:22', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (45, '61. 旋转链表', '<p>给定一个链表，旋转链表，将链表每个节点向右移动&nbsp;k&nbsp;个位置，其中&nbsp;k&nbsp;是非负数。</p>\n<p>示例&nbsp;1:</p>\n<p>输入: 1-&gt;2-&gt;3-&gt;4-&gt;5-&gt;NULL, k = 2<br />输出: 4-&gt;5-&gt;1-&gt;2-&gt;3-&gt;NULL<br />解释:<br />向右旋转 1 步: 5-&gt;1-&gt;2-&gt;3-&gt;4-&gt;NULL<br />向右旋转 2 步: 4-&gt;5-&gt;1-&gt;2-&gt;3-&gt;NULL<br />示例&nbsp;2:</p>\n<p>输入: 0-&gt;1-&gt;2-&gt;NULL, k = 4<br />输出: 2-&gt;0-&gt;1-&gt;NULL<br />解释:<br />向右旋转 1 步: 2-&gt;0-&gt;1-&gt;NULL<br />向右旋转 2 步: 1-&gt;2-&gt;0-&gt;NULL<br />向右旋转 3 步:&nbsp;0-&gt;1-&gt;2-&gt;NULL<br />向右旋转 4 步:&nbsp;2-&gt;0-&gt;1-&gt;NULL</p>\n<hr />\n<p>思路： 环链表法<br />计算链表的长度N。</p>\n<p>k对N取模。 r=k%N。</p>\n<p>当 r == 0 时，不需要旋转。</p>\n<p>当 r &gt; 0 时，右移r个节点即可。</p>\n<p>将链表的尾部指向头部，构成循环链表。并在从开始的N-r个节点后面将链表断开。第N-r+1个节点就是新的头节点。</p>\n<p>例如：1-&gt;2-&gt;3-&gt;4-&gt;5-&gt;NULL, k = 2</p>\n<p>链表长度N=5. 尾部连到头部构成环。 1-&gt;2-&gt;3-&gt;4-&gt;5-&gt;</p>\n<p>r=k%N=2%5=2. 右移2个节点。在第N-r=5-2=3个节点后面断开，第N-r+1=5-2+1=4节点就是新的头节点。</p>\n<pre class=\"language-java\"><code> public ListNode rotateRight(ListNode head, int k) {\n        if(head==null)\n            return null;\n        int len=1;\n        ListNode tem=head;\n        while(tem.next!=null)//第一次遍历找到长度和尾结点\n        {\n            len++;\n            tem=tem.next;\n        }\n        tem.next=head;//首尾相连组成循环链表\n        int btnum=k%len;//真正需要右移的次数\n        ListNode interruptNode=head;    \n        for (int i = 0; i &lt; len - btnum-1; i++) {//第二次遍历找到断开节点\n            interruptNode=interruptNode.next;\n        }\n        ListNode res=interruptNode.next;\n        interruptNode.next=null;\n        return res;\n    }</code></pre>\n<hr />\n<h3>提交记录</h3>\n<div id=\"details-summary\">\n<table>\n<tbody>\n<tr>\n<td>\n<div id=\"result_progress_row\" class=\"row\"><strong><span id=\"result_progress\" class=\"ng-binding\">231 / 231</span></strong>&nbsp;个通过测试用例</div>\n</td>\n<td id=\"status\">状态：\n<h4 id=\"result_state\" class=\"inline-wrap ng-binding text-success\">通过</h4>\n</td>\n</tr>\n<tr>\n<td>\n<div id=\"ac_output\" class=\"row\">执行用时：<strong><span id=\"result_runtime\" class=\"ng-binding\">1 ms</span></strong></div>\n</td>\n<td>\n<div id=\"submitted-time\">提交时间：<strong><span id=\"result_date\">1&nbsp;小时，33&nbsp;分钟之前</span></strong></div>\n</td>\n</tr>\n</tbody>\n</table>\n</div>', '61. 旋转链表', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 294, 2, '2019-12-03 13:35:34', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (46, '82. 删除排序链表中的重复元素 II', '<p>给定一个排序链表，删除所有含有重复数字的节点，只保留原始链表中&nbsp;没有重复出现&nbsp;的数字。</p>\n<p>示例&nbsp;1:</p>\n<p>输入: 1-&gt;2-&gt;3-&gt;3-&gt;4-&gt;4-&gt;5<br />输出: 1-&gt;2-&gt;5</p>\n<p><br />示例&nbsp;2:</p>\n<p>输入: 1-&gt;1-&gt;1-&gt;2-&gt;3<br />输出: 2-&gt;3</p>\n<hr />\n<p>思路一：使用map记录节点值将所有出现次数大于1的值删除。需要遍历两次并且使用散列表辅助空间。</p>\n<pre class=\"language-java\"><code>ListNode deleteDuplicates(ListNode head) {\n        Map&lt;Integer,Boolean&gt; map=new HashMap&lt;&gt;();\n        ListNode tem=head;\n        while(tem!=null){\n            if(map.containsKey(tem.val)){\n                map.put(tem.val,true);\n            }else{\n                map.put(tem.val,false);\n            }\n            tem=tem.next;\n        }\n        ListNode node=new ListNode(0);\n        node.next=head;\n        ListNode res=node;\n        while(node.next!=null){\n            if(map.get(node.next.val))\n            {\n                node.next=node.next.next;\n            }else {\n                node=node.next;\n            }\n        }\n        return res.next;\n    }</code></pre>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">执行用时 :5 ms, 在所有&nbsp;java&nbsp;提交中击败了5.79%的用户</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">内存消耗 :36.6 MB, 在所有&nbsp;java&nbsp;提交中击败了62.44%的用户</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">嗯。。。慢的一批。。尝试其他思路。。</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\"><hr /></div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">思路二：对于每一个节点比较 它后面两个元素是否相等，如果相等继续判断第三，四，五...个是否相等，然后删除这些节点。如果不相等，往后遍历。</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">\n<pre class=\"language-java\"><code>  public ListNode deleteDuplicates(ListNode head) {\n        ListNode lshead = new ListNode(0);//接一个头结点\n        lshead.next = head;\n        ListNode tem = lshead;//正在遍历的节点\n        ListNode prenode = tem.next;//遍历节点的后继\n        ListNode nextnode = null;//遍历节点的后继的后继\n        if (prenode != null) {\n            nextnode = prenode.next;\n        }\n       while (prenode != null &amp;&amp; nextnode != null) {\n            while (nextnode != null&amp;&amp;prenode.val == nextnode.val) {//相等则nextnode继续往后遍历\n                nextnode = nextnode.next;\n                tem.next = nextnode;\n            }\n            if(nextnode!=null){//删除重复节点\n                prenode = tem.next;\n                nextnode = prenode.next;\n            }\n            while (nextnode != null &amp;&amp; prenode.val != nextnode.val) {//不相等往后遍历\n                tem = tem.next;\n                prenode = tem.next;\n                nextnode = tem.next.next;\n\n            }\n        }\n        return lshead.next;\n    }</code></pre>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">执行用时 :1 ms, 在所有&nbsp;java&nbsp;提交中击败了98.53%的用户</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">内存消耗 :36.9 MB, 在所有&nbsp;java&nbsp;提交中击败了59.25%的用户</div>\n</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">&nbsp;</div>', '82. 删除排序链表中的重复元素 II', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 328, 2, '2019-12-03 13:54:23', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (47, '46. 全排列', '<p>给定一个没有重复数字的序列，返回其所有可能的全排列。</p>\n<p>示例:</p>\n<p>输入: [1,2,3]<br />输出:<br />[<br />[1,2,3],<br />[1,3,2],<br />[2,1,3],<br />[2,3,1],<br />[3,1,2],<br />[3,2,1]<br />]</p>\n<hr />\n<div>思路：</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; 回溯法，分别将（第一个，第二个...第nums.length-1个）元素和首个元素交换</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; 1{2,3,4}</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; 2{1,3,4}</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; 3{1,2,4}</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; 4{1,2,3}</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; &nbsp;&nbsp; &nbsp;递归执行{2,3,4}&nbsp;{1,3,4}&nbsp;{1,2,4}&nbsp;{1,2,3} 的全排序</div>\n<div>如图：</div>\n<div><img src=\"https://pic.leetcode-cn.com/0bf18f9b86a2542d1f6aa8db6cc45475fce5aa329a07ca02a9357c2ead81eec1-image.png\" alt=\"\" width=\"1000\" height=\"530\" /></div>\n<div>&nbsp; &nbsp;</div>\n<div>\n<pre class=\"language-java\"><code>class Solution {\n    public List&lt;List&lt;Integer&gt;&gt; permute(int[] nums) {\n        List&lt;List&lt;Integer&gt;&gt; res=new ArrayList&lt;&gt;();\n        backstack(nums,0,nums.length,res);\n        return res;\n    }\n\n    private void backstack(int[] nums,int begin,int end, List&lt;List&lt;Integer&gt;&gt; res){\n\n        if(begin==end){\n            List&lt;Integer&gt; tem = new ArrayList&lt;Integer&gt;();\n            for (int n = 0; n &lt; nums.length; n++) {\n                tem.add(nums[n]);\n            }\n            res.add(tem);\n            return;\n        }\n        for (int i = begin; i &lt;end; i++) {//begin-end 之间的全排列\n            swap(nums,i,begin);\n            backstack(nums,begin+1,end,res);\n            swap(nums,i,begin);\n\n        }\n    }\n\n    private  void swap(int[] nums, int x, int y) {\n        int a = nums[x];\n        nums[x] = nums[y];\n        nums[y] = a;\n    }\n}</code></pre>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\"><hr /><img class=\"wscnph\" src=\"\" /></div>\n</div>', '46. 全排列', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 299, 2, '2019-12-03 14:07:34', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (48, 'Java并发工具包中的Fork/Join框架', '<div>ForkJoin是由JDK1.7后提供多线并发处理框架。ForkJoin的框架的基本思想是分而治之。ForkJoin框架其实就是一个线程池ExecutorService的实现，通过工作窃取(work-stealing)算法,获取其他线程中未完成的任务来执行。可以充分利用机器的多处理器优势，利用空闲的线程去并行快速完成一个可拆分为小任务的大任务，类似于分治算法。ForkJoin的目标，就是利用所有可用的处理能力来提高程序的响应和性能。<br /><br /></div>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/3047136-2044de505345b0a1.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp\" alt=\"\" width=\"1200\" height=\"536\" /></p>\n<h2>基础特性</h2>\n<p><img src=\"https://img-blog.csdnimg.cn/2019032123460544.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NvZGluZ3R1,size_16,color_FFFFFF,t_70\" alt=\"\" width=\"948\" height=\"718\" /></p>\n<p>ForkJoin框架的核心是ForkJoinPool类，基于AbstractExecutorService扩展。ForkJoinPool中维护了一个队列数组WorkQueue[],每个WorkQueue维护一个ForkJoinTask数组和当前工作线程。ForkJoinPool实现了工作窃取(work-stealing)算法并执行ForkJoinTask。</p>\n<hr />\n<h2>案例使用</h2>\n<p>计算从1-100的和，使用Fork-Join分而治之</p>\n<pre class=\"language-java\"><code>public class ForkJoinDemo {\n    class CountRecursiveTask extends RecursiveTask&lt;Integer&gt; {\n        //达到子任务直接计算的阈值\n        private  final int Th = 10;\n\n        private int start;\n        private int end;\n\n        public CountRecursiveTask(int start, int end) {\n            this.start = start;\n            this.end = end;\n        }\n\n        @Override\n        protected Integer compute() {\n\n            if (end - start &lt;= Th) {\n                return count();//如果小于阈值，直接调用最小任务的计算方法\n            } else {  //如果仍大于阈值，则继续拆分为2个子任务，分别调用fork方法。\n                int middle = (end + start) / 2;\n                System.out.println(\"start:\" + start + \";middle:\" + middle + \";end:\" + end);\n                CountRecursiveTask left = new CountRecursiveTask(start, middle);//拆分左边\n                left.fork();\n                CountRecursiveTask right = new CountRecursiveTask(middle + 1, end);//拆分右边\n                right.fork();\n                return right.join() + left.join();\n            }\n\n\n        }\n\n        private int count() {\n            int sum = 0;\n            for (int i = start; i &lt;= end; i++) {\n                sum += i;\n            }\n            return sum;\n        }\n    }\n\n\n    public static void main(String[] args) {\n        ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());\n\n        Integer sum = forkJoinPool.invoke(new ForkJoinDemo().new CountRecursiveTask(1, 100));\n        System.out.println(sum);\n\n    }\n}</code></pre>\n<pre class=\"language-java\"><code>start:1;middle:50;end:100\nstart:1;middle:25;end:50\nstart:51;middle:75;end:100\nstart:1;middle:13;end:25\nstart:1;middle:7;end:13\nstart:14;middle:19;end:25\nstart:51;middle:63;end:75\nstart:51;middle:57;end:63\nstart:64;middle:69;end:75\nstart:76;middle:88;end:100\nstart:76;middle:82;end:88\nstart:89;middle:94;end:100\nstart:26;middle:38;end:50\nstart:26;middle:32;end:38\nstart:39;middle:44;end:50\n5050</code></pre>\n<pre class=\"language-markup\"><code>要注意的是两个任务都 fork 的情况，必须按照 f1.fork()，f2.fork()， f2.join()，f1.join() 这样的顺序，不然有性能问题。JDK官方文档有说明，有兴趣的可以去研究下。</code></pre>\n<p>有一点需要注意，Fork-Join框架只是在同时使用多核运行原来的单核程序，每个核心上的程序之间互不交叉。这不会改变算法原来的时间复杂度。比如查询一个超大数组中的最小值，复杂度为O(n),通过Fork-Join框架，将数组分为两部分，递归的查询每部分的最小值，这只是将原来的所有操作用两个核心运行，时间复杂度为O(n/2)，其实依然是O(n)。所以，假如排序一亿个数，单核用时30s，双核的用时也不会少于15s，因此，想要通过Fork-Join指数级提升性能是不现实的。但是对于已经成熟的系统，每一个微小部分的提升都有助于提升系统整体性能，从这个角度讲，Fork-Join还是有极大的用处。</p>\n<p>&nbsp;</p>\n<hr />\n<h2 id=\"2-1-工作顺序图\">工作顺序图</h2>\n<p>下图展示了以上代码的工作过程概要，但实际上Fork/Join框架的内部工作过程要比这张图复杂得多，例如如何决定某一个recursive task是使用哪条线程进行运行；再例如如何决定当一个任务/子任务提交到Fork/Join框架内部后，是创建一个新的线程去运行还是让它进行队列等待。</p>\n<p>所以如果不深入理解Fork/Join框架的运行原理，只是根据之上最简单的使用例子观察运行效果，那么我们只能知道子任务在Fork/Join框架中被拆分得足够小后，并且其内部使用多线程并行完成这些小任务的计算后再进行结果向上的合并动作，最终形成顶层结果。我们先从这张概要的过程图开始讨论。</p>\n<p><img src=\"https://img-blog.csdn.net/20170511170511140?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWlud2Vuamll/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast\" alt=\"\" width=\"730\" height=\"520\" /></p>\n<p>图中最顶层的任务使用submit方式被提交到Fork/Join框架中，后者将前者放入到某个线程中运行，工作任务中的compute方法的代码开始对这个任务T1进行分析。如果当前任务需要累加的数字范围过大（代码中设定的是大于200），则将这个计算任务拆分成两个子任务（T1.1和T1.2），每个子任务各自负责计算一半的数据累加，请参见代码中的fork方法。如果当前子任务中需要累加的数字范围足够小（小于等于200），就进行累加然后返回到上层任务中。</p>\n<hr />\n<h2 id=\"2-2-forkjoinpool构造函数\">&nbsp;ForkJoinPool构造函数</h2>\n<p>ForkJoinPool有四个构造函数，其中参数最全的那个构造函数如下所示：</p>\n<pre class=\"language-java\"><code>public ForkJoinPool(int parallelism,\n                        ForkJoinWorkerThreadFactory factory,\n                        UncaughtExceptionHandler handler,\n                        boolean asyncMode)</code></pre>\n<ul>\n<li>\n<p>parallelism：可并行级别，Fork/Join框架将依据这个并行级别的设定，决定框架内并行执行的线程数量。并行的每一个任务都会有一个线程进行处理，但是千万不要将这个属性理解成Fork/Join框架中最多存在的线程数量，也不要将这个属性和ThreadPoolExecutor线程池中的corePoolSize、maximumPoolSize属性进行比较，因为ForkJoinPool的组织结构和工作方式与后者完全不一样。而后续的讨论中，读者还可以发现Fork/Join框架中可存在的线程数量和这个参数值的关系并不是绝对的关联（有依据但并不全由它决定）。</p>\n</li>\n<li>\n<p>factory：当Fork/Join框架创建一个新的线程时，同样会用到线程创建工厂。只不过这个线程工厂不再需要实现ThreadFactory接口，而是需要实现ForkJoinWorkerThreadFactory接口。后者是一个函数式接口，只需要实现一个名叫newThread的方法。在Fork/Join框架中有一个默认的ForkJoinWorkerThreadFactory接口实现：DefaultForkJoinWorkerThreadFactory。</p>\n</li>\n<li>\n<p>handler：异常捕获处理器。当执行的任务中出现异常，并从任务中被抛出时，就会被handler捕获。</p>\n</li>\n<li>\n<p>asyncMode：这个参数也非常重要，从字面意思来看是指的异步模式，它并不是说Fork/Join框架是采用同步模式还是采用异步模式工作。Fork/Join框架中为每一个独立工作的线程准备了对应的待执行任务队列，这个任务队列是使用数组进行组合的双向队列。即是说存在于队列中的待执行任务，即可以使用先进先出的工作模式，也可以使用后进先出的工作模式。（当asyncMode设置为ture的时候，队列采用先进先出方式工作；反之则是采用后进先出的方式工作，该值默认为false）</p>\n</li>\n</ul>\n<p>ForkJoinPool还有另外两个构造函数，一个构造函数只带有parallelism参数，既是可以设定Fork/Join框架的最大并行任务数量；另一个构造函数则不带有任何参数，对于最大并行任务数量也只是一个默认值&mdash;&mdash;当前操作系统可以使用的CPU内核数量（Runtime.getRuntime().availableProcessors()）。实际上ForkJoinPool还有一个私有的、原生构造函数，之上提到的三个构造函数都是对这个私有的、原生构造函数的调用。</p>\n<p>如果你对Fork/Join框架没有特定的执行要求，可以直接使用不带有任何参数的构造函数。也就是说推荐基于当前操作系统可以使用的CPU内核数作为Fork/Join框架内最大并行任务数量，这样可以保证CPU在处理并行任务时，尽量少发生任务线程间的运行状态切换（实际上单个CPU内核上的线程间状态切换基本上无法避免，因为操作系统同时运行多个线程和多个进程）。</p>\n<hr />\n<h2 id=\"2-3-fork方法和join方法\">&nbsp;fork方法和join方法</h2>\n<p>Fork/Join框架中提供的fork方法和join方法，可以说是该框架中提供的最重要的两个方法，它们和parallelism&ldquo;可并行任务数量&rdquo;配合工作，可以导致拆分的子任务T1.1、T1.2甚至TX在Fork/Join框架中不同的运行效果。例如TX子任务或等待其它已存在的线程运行关联的子任务，或在运行TX的线程中&ldquo;递归&rdquo;执行其它任务，又或者启动一个新的线程运行子任务&hellip;&hellip;</p>\n<p>fork方法用于将新创建的子任务放入当前线程的work queue队列中，Fork/Join框架将根据当前正在并发执行ForkJoinTask任务的ForkJoinWorkerThread线程状态，决定是让这个任务在队列中等待，还是创建一个新的ForkJoinWorkerThread线程运行它，又或者是唤起其它正在等待任务的ForkJoinWorkerThread线程运行它。</p>\n<p>这里面有几个元素概念需要注意，ForkJoinTask任务是一种能在Fork/Join框架中运行的特定任务，也只有这种类型的任务可以在Fork/Join框架中被拆分运行和合并运行。ForkJoinWorkerThread线程是一种在Fork/Join框架中运行的特性线程，它除了具有普通线程的特性外，最主要的特点是<strong>每一个ForkJoinWorkerThread线程都具有一个独立的任务等待队列（work queue）</strong>，这个任务队列用于存储在本线程中被拆分的若干子任务。</p>\n<p><img src=\"https://img-blog.csdn.net/20170514084721521?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWlud2Vuamll/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast\" alt=\"\" width=\"673\" height=\"339\" /></p>\n<p>join方法用于让当前线程阻塞，直到对应的子任务完成运行并返回执行结果。或者，如果这个子任务存在于当前线程的任务等待队列（work queue）中，则取出这个子任务进行&ldquo;递归&rdquo;执行。其目的是尽快得到当前子任务的运行结果，然后继续执行。</p>\n<hr />\n<p><a href=\"https://blog.csdn.net/tyrroo/article/details/81390202\" target=\"_blank\" rel=\"noopener\">点击查看原文</a></p>', 'ForkJoin是由JDK1.7后提供多线并发处理框架。ForkJoin的框架的基本思想是分而治之。什么是分而治之？分而治之就是将一个复杂的计算，按照设定的阈值进行分解成多个计算，然后将各个计算结果进行汇总。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 328, 1, '2019-12-07 10:46:25', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (49, 'JVM体系结构', '<h1>JVM体系结构图：</h1>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/6423761-0e92ccf1175a7962.png?imageMogr2/auto-orient/strip|imageView2/2/w/859/format/webp\" alt=\"\" width=\"859\" height=\"738\" /></p>\n<div>\n<h1>JVM字节码文件（JVM Languages Classes ）</h1>\n<p>包括但不仅限于Java语言编译而成的Class文件。实际上，Java虚拟机不和包括Java在内的任何编程语言绑定，它只与&ldquo;Class文件&rdquo;这种特定的二进制文件格式关联，只要特定语言的编译器能将代码编译成Class文件，虚拟机并不关心Class的来源是何种语言，如下图所示</p>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/6423761-6a635c8b3dab4725.png?imageMogr2/auto-orient/strip|imageView2/2/w/854/format/webp\" alt=\"\" width=\"854\" height=\"426\" /></p>\n<div>\n<h1>类加载器（Class Loader）</h1>\n<p>Class文件需要被加载到内存里才能得以运行和使用。虚拟机把Class文件加载到内存后，对数据进行验证、转换解析和初始化，最终形成可以被虚拟机直接使用的Java类型，这就是虚拟机的类加载机制。具体内容，我们将在之后的篇幅进行详细一点的介绍。</p>\n<h1>运行时数据区（Runtime Data Area）</h1>\n<p>Java虚拟机在运行程序的过程中会把它所管理的内存划分为若干个不同的数据区，上图&ldquo;JVM Architecture&rdquo;中，基于内存是否能被线程所共享，内存被分为了蓝色和白色两大块区域，蓝色区域表示所有线程都会向此区域读写数据，白色区域表示这些区域是线程私有的，每条线程都有自己的虚拟机栈、本地方法栈、程序计数器，各条线程之间的栈和计数器相互隔离。它们之间的关系可以表示为下图：</p>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/6423761-c44a15402a52ad80.png?imageMogr2/auto-orient/strip|imageView2/2/w/769/format/webp\" alt=\"\" width=\"769\" height=\"465\" /></p>\n<p>各数据区的作用将在下一个章节开始详细介绍。</p>\n<h1>执行引擎（Execution Engine）</h1>\n<p>执行字节码指令，该区域包括解释器、编译器和垃圾回收器</p>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/6423761-8c71267d38fa1fc2.png?imageMogr2/auto-orient/strip|imageView2/2/w/489/format/webp\" alt=\"\" width=\"489\" height=\"244\" /></p>\n<div>\n<ul>\n<li>解释器：解释器更快地解释字节码，但执行缓慢。解释器的缺点是当一个方法被调用多次时，每次都需要一个新的解释</li>\n<li>JIT编译器：JIT编译器消除了解释器的缺点。执行引擎将在转换字节码时使用解释器的帮助，但是当它发现重复的代码时，将使用JIT编译器，它编译整个字节码并将其更改为本地代码。这个本地代码将直接用于重复的方法调用，这提高了系统的性能</li>\n<li>垃圾收集器：收集和删除未引用的对象，来释放内存空间。</li>\n</ul>\n<div>\n<div>\n<h1>本地库接口（Native Interface）</h1>\n<p>提供一个标准的方式让Java程序通过虚拟机与原生代码进行交互，这也就是我们平常常说的Java本地接口（JNI&mdash;&mdash;Java Native Interface）。它使得在 JVM 内部运行的Java 代码能够与用其它编程语言（如 C、C++ 和汇编语言）编写的应用程序和库进行互操作。JNI最重要的好处是它没有对底层 Java 虚拟机的实现施加任何限制。因此，Java虚拟机厂商可以在不影响虚拟机其它部分的情况下添加对JNI的支持。程序员只需编写一种版本的本地应用程序或库，就能够与所有支持JNI 的Java 虚拟机协同工作。</p>\n<h1>本地方法库（Native Libraires）</h1>\n<p>它是执行引擎所需的本机库的集合</p>\n<hr />\n<p><a href=\"https://www.jianshu.com/p/33948336306f\" target=\"_blank\" rel=\"noopener\">点击查看原文</a></p>\n</div>\n</div>\n</div>\n</div>\n</div>', 'JVM全称Java Virtual Machine（Java虚拟机），是一个虚构出来的计算机，它屏蔽了与具体操作系统平台相关的信息，使得Java程序只需生成在Java虚拟机上运行的目标代码（字节码，ByteCode）, 就可以在多种平台上不加修改地运行。这背后其实就是JVM把字节码翻译成具体平台上的机器指令，从而实现“一次编写，到处运行（Write Once, Run Anywhere）”', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 297, 5, '2019-12-07 11:46:47', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (50, 'JVM的三种类加载器和双亲委派机制', '<p>JVM中的类的加载器主要有三种:启动类加载器，拓展类加载器，应用类加载器。</p>\n<p>&nbsp; &nbsp; &nbsp;启动类加载器(Bootstrap classLoader):又称为引导类加载器，由C++编写，无法通过程序得到。主要负责加载JAVA中的 一些核心类库，主要是位于&lt;JAVA_HOME&gt;/lib/rt.jar中。</p>\n<p>&nbsp; &nbsp; &nbsp;拓展类加载器(Extension classLoader):主要加载JAVA中的一些拓展类，位于&lt;JAVA_HOME&gt;/lib/ext中,是启动类加载器的子类。</p>\n<p>&nbsp; &nbsp; &nbsp;应用类加载器(System classLoader): 又称为系统类加载器,主要用于加载CLASSPATH路径下我们自己写的类，是拓展类加载器的子类。<br /><br /></p>\n<p><img src=\"https://oscimg.oschina.net/oscnet/5c3578c7229c3a17ce647596563bb95a351.jpg\" alt=\"\" width=\"625\" height=\"458\" /></p>\n<h2>验证：</h2>\n<pre class=\"language-java\"><code>public class Hello {\n    public static void main(String[] args) {\n        Object object=new Object();//系统类 位于rt.jar中\n        Hello hello=new Hello();// 我们自定义的类\n        System.out.println(object.getClass().getClassLoader());\n        System.out.println(hello.getClass().getClassLoader());\n        System.out.println(hello.getClass().getClassLoader().getParent());\n        System.out.println(hello.getClass().getClassLoader().getParent().getParent());\n    }\n}</code></pre>\n<pre class=\"language-java\"><code>null\nsun.misc.Launcher$AppClassLoader@18b4aac2\nsun.misc.Launcher$ExtClassLoader@1540e19d\nnull</code></pre>\n<p>.getClassLoader().getParent().getParent()与object.getClass().getClassLoader()得到将会是Null，因为启动类（Bootstrap classLoader)）加载器是用C++写的，我们无法通过程序直接得到.</p>\n<hr />\n<h1>双亲委派</h1>\n<p>双亲委派的意思是如果一个类加载器需要加载类，那么首先它会把这个类请求委派给父类加载器去完成，每一层都是如此。一直递归到顶层，当父加载器无法完成这个请求时，子类才会尝试去加载。</p>\n<h2><span class=\"bjh-p\">双亲委派有啥好处呢？</span></h2>\n<p><span class=\"bjh-p\">它使得类有了层次的划分。就拿java.lang.Object来说，你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的，也就是最终都是由Bootstrap ClassLoader去找&lt;JAVA_HOME&gt;\\lib中rt.jar里面的java.lang.Object加载到JVM中。</span></p>\n<p><span class=\"bjh-p\">这样如果有不法分子自己造了个java.lang.Object,里面嵌了不好的代码，如果我们是按照双亲委派模型来实现的话，最终加载到JVM中的只会是我们rt.jar里面的东西，也就是这些核心的基础类代码得到了保护。因为这个机制使得系统中只会出现一个java.lang.Object。不会乱套了。你想想如果我们JVM里面有两个Object,那岂不是天下大乱了。</span></p>', '  JVM中的类的加载器主要有三种:启动类加载器，拓展类加载器，应用类加载器。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 310, 5, '2019-12-07 15:57:36', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (51, 'JVM栈(stack)', '<h1>定义</h1>\n<p>每个JVM线程拥有一个私有的 Java虚拟机栈，创建线程的同时栈也被创建。一个JVM栈由许多帧组成，称之为<span class=\"hljs-string\">\"栈帧\"</span>。JVM中的栈和<span class=\"hljs-keyword\">C</span>等常见语言中的栈比较类似，都用于保存局部变量和部分计算结果，同时也参与方法调用和返回。</p>\n<p>对于栈来说不存在垃圾回收问题。只要线程一结束该栈则Over，生命周期和所在线程一致，是线程私有。</p>\n<p><span style=\"color: #ff0000;\">8中基本类型变量+对象的引用变量+实例方法都是在函数栈内存分配。</span></p>\n<h2>栈存储什么？</h2>\n<p>本地变量（Local Variables）：输入参数和输出参数以及方法内的变量；</p>\n<p>栈操作（Operand Stack）：记录出栈，入栈的操作；</p>\n<p>栈帧数据（Frame Data）：包括类文件，方法等；</p>\n<h1>栈结构</h1>\n<p>栈帧（Stack Frame）是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的java虚拟机栈的栈元素。</p>\n<p>栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。</p>\n<p><img src=\"https://img2018.cnblogs.com/blog/858186/201907/858186-20190711154743556-950500902.png\" alt=\"\" /></p>\n<h2>局部变量表</h2>\n<p>1.局部变量表（Local Variable Table）是一组变量值存储空间，用于存放方法参数和方法内部定义的局部变量。&nbsp;并且在Java编译为Class文件时，就已经确定了该方法所需要分配的局部变量表的最大容量。</p>\n<p>2.局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」，对象引用(reference类型) 和 returnAddress类型（它指向了一条字节码指令的地址）</p>\n<hr />\n<p>　注意：</p>\n<p>　　很多人说：基本数据和对象引用存储在栈中。</p>\n<p>　　当然这种说法虽然是正确的，但是很不严谨，只能说这种说法针对的是局部变量。</p>\n<p>　　局部变量存储在局部变量表中，随着线程而生，线程而灭。并且线程间数据不共享。</p>\n<p>　　但是，如果是成员变量，或者定义在方法外对象的引用，它们存储在堆中。</p>\n<p>　　因为在堆中，是线程共享数据的，并且栈帧里的命名就已经清楚的划分了界限 : 局部变量表！</p>\n<hr />\n<h2>操作数栈</h2>\n<ul>\n<li>与局部变量表一样，均以字长为单位的数组。不过局部变量表用的是索引，操作数栈是弹栈/压栈来访问。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。</li>\n<li>存储的数据与局部变量表一致含int、long、float、double、reference、returnType，操作数栈中byte、short、char压栈前(bipush)会被转为int。</li>\n<li>数据运算的地方，大多数指令都在操作数栈弹栈运算，然后结果压栈。</li>\n<li>java虚拟机栈是方法调用和执行的空间，每个方法会封装成一个栈帧压入占中。其中里面的操作数栈用于进行运算，当前线程只有当前执行的方法才会在操作数栈中调用指令（可见java虚拟机栈的指令主要取于操作数栈）。</li>\n<li>如果int类型在-128~127(这个就直接存在常量池了)</li>\n</ul>\n<h2>动态连接</h2>\n<p>　　每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用，</p>\n<p>　　持有这个引用是为了支持方法调用过程中的动态连接（Dynamic Linking）。</p>\n<p>　　在类加载阶段中的解析阶段会将符号引用转为直接引用，这种转化也称为静态解析。</p>\n<p>　　另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接。</p>\n<p>　　这里简单提一下动态连接的概念,后面在详细讲解.</p>\n<h2>方法出口</h2>\n<p>　　当一个方法开始执行后，只有2种方式可以退出这个方法 ：</p>\n<p>　　方法返回指令&nbsp;： 执行引擎遇到一个方法返回的字节码指令，这时候有可能会有返回值传递给上层的方法调用者，这种退出方式称为正常完成出口。</p>\n<p>　　异常退出&nbsp;： 在方法执行过程中遇到了异常，并且没有处理这个异常，就会导致方法退出。</p>\n<p>　　无论采用任何退出方式，在方法退出之后，都需要返回到方法被调用的位置，程序才能继续执行，方法返回时可能需要在栈帧中保存一些信息。</p>\n<p>　　　　一般来说，方法正常退出时，调用者的PC计数器的值可以作为返回地址，栈帧中会保存这个计数器值。</p>\n<p>　　而方法异常退出时，返回地址是要通过异常处理器表来确定的，栈帧中一般不会保存这部分信息。</p>', '每个JVM线程拥有一个私有的 Java虚拟机栈，创建线程的同时栈也被创建。一个JVM栈由许多帧组成，称之为\"栈帧\"。JVM中的栈和C等常见语言中的栈比较类似，都用于保存局部变量和部分计算结果，同时也参与方法调用和返回。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 333, 5, '2019-12-07 18:52:11', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (52, 'JVM堆内存(heap)', '<p>JAVA堆内存管理是影响性能主要因素之一。<br />堆内存溢出是JAVA项目非常常见的故障，在解决该问题之前，必须先了解下JAVA堆内存是怎么工作的。</p>\n<p>先看下JAVA堆内存是如何划分的，如图：</p>\n<p><img src=\"http://i2.51cto.com/images/blog/201808/21/b116170771ecb3117ae7fead03fcaa0d.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=\" alt=\"\" /></p>\n<ol>\n<li>JVM内存划分为堆内存和非堆内存，堆内存分为年轻代（Young Generation）、老年代（Old Generation），非堆内存就一个永久代（Permanent Generation）。</li>\n<li>年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量，Survivor两个区占小容量，默认比例是8:1:1。</li>\n<li>堆内存用途：存放的是对象，垃圾收集器就是收集这些对象，然后根据GC算法回收。</li>\n<li>非堆内存用途：永久代，也称为方法区，存储程序运行时长期存活的对象，比如类的元数据、方法、常量、属性等。</li>\n</ol>\n<p>在JDK1.8版本废弃了永久代，替代的是元空间（MetaSpace），元空间与永久代上类似，都是方法区的实现，他们最大区别是：元空间并不在JVM中，而是使用本地内存。<br />元空间有注意有两个参数：</p>\n<ul>\n<li>MetaspaceSize ：初始化元空间大小，控制发生GC阈值</li>\n<li>MaxMetaspaceSize ： 限制元空间大小上限，防止异常占用过多物理内存</li>\n</ul>\n<h2>为什么移除永久代？</h2>\n<p>移除永久代原因：为融合HotSpot JVM与JRockit VM（新JVM技术）而做出的改变，因为JRockit没有永久代。<br />有了元空间就不再会出现永久代OOM问题了！</p>\n<h2>分代概念</h2>\n<p>新生成的对象首先放到年轻代Eden区，当Eden空间满了，触发Minor GC，存活下来的对象移动到Survivor0区，Survivor0区满后触发执行Minor GC，Survivor0区存活对象移动到Suvivor1区，这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。<br />老年代存储长期存活的对象，占满时会触发Major GC=Full GC，GC期间会停止所有线程等待GC完成，所以对响应要求高的应用尽量减少发生Major GC，避免响应超时。<br />Minor GC ： 清理年轻代&nbsp;<br />Major GC ： 清理老年代<br />Full GC ： 清理整个堆空间，包括年轻代和永久代<br />所有GC都会停止应用所有线程。</p>\n<h2>为什么分代？</h2>\n<p>将对象根据存活概率进行分类，对存活时间长的对象，放到固定区，从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法，对算法扬长避短。</p>\n<h2>为什么survivor分为两块相等大小的幸存空间？</h2>\n<p>主要为了解决碎片化。如果内存碎片化严重，也就是两个对象占用不连续的内存，已有的连续内存不够新对象存放，就会触发GC。</p>\n<h2>JVM堆内存常用参数</h2>\n<div class=\"table-box\">\n<table>\n<thead>\n<tr>\n<th>参数</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>-Xms</td>\n<td>堆内存初始大小，单位m、g</td>\n</tr>\n<tr>\n<td>-Xmx（MaxHeapSize）</td>\n<td>堆内存最大允许大小，一般不要大于物理内存的80%</td>\n</tr>\n<tr>\n<td>-XX:PermSize</td>\n<td>非堆内存初始大小，一般应用设置初始化200m，最大1024m就够了</td>\n</tr>\n<tr>\n<td>-XX:MaxPermSize</td>\n<td>非堆内存最大允许大小</td>\n</tr>\n<tr>\n<td>-XX:NewSize（-Xns）</td>\n<td>年轻代内存初始大小</td>\n</tr>\n<tr>\n<td>-XX:MaxNewSize（-Xmn）</td>\n<td>年轻代内存最大允许大小，也可以缩写</td>\n</tr>\n<tr>\n<td>-XX:SurvivorRatio=8</td>\n<td>年轻代中Eden区与Survivor区的容量比例值，默认为8，即8:1</td>\n</tr>\n<tr>\n<td>-Xss</td>\n<td>堆栈内存大小</td>\n</tr>\n</tbody>\n</table>\n<p>&nbsp;</p>\n<h2>垃圾回收算法（GC，Garbage Collection）</h2>\n<p>红色是标记的非活动对象，绿色是活动对象。</p>\n<ul>\n<li><strong>标记-清除（Mark-Sweep）</strong><br />GC分为两个阶段，标记和清除。首先标记所有可回收的对象，在标记完成后统一回收所有被标记的对象。同时会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时，无法找到足够的连续内存，而不得已再次触发GC。</li>\n</ul>\n<p style=\"padding-left: 40px;\"><img src=\"http://i2.51cto.com/images/blog/201808/21/248dbe09bbf26285cba1760ad0355faa.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=\" alt=\"\" /></p>\n<ul>\n<li><strong>复制（Copy）</strong><br />将内存按容量划分为两块，每次只使用其中一块。当这一块内存用完了，就将存活的对象复制到另一块上，然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收，也不用考虑内存碎片问题，简单高效。缺点需要两倍的内存空间。</li>\n</ul>\n<p style=\"padding-left: 40px;\"><img src=\"http://i2.51cto.com/images/blog/201808/21/55b07a315eefb27aab131363cdf328ec.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=\" alt=\"\" /></p>\n<ul>\n<li><strong>标记-整理（Mark-Compact）</strong><br />也分为两个阶段，首先标记可回收的对象，再将存活的对象都向一端移动，然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片问题，同时也避免了复制算法的空间问题。<br />一般年轻代中执行GC后，会有少量的对象存活，就会选用复制算法，只要付出少量的存活对象复制成本就可以完成收集。而老年代中因为对象存活率高，没有额外过多内存空间分配，就需要使用标记-清理或者标记-整理算法来进行回收。</li>\n</ul>\n<p style=\"padding-left: 40px;\"><img src=\"http://i2.51cto.com/images/blog/201808/21/70a593b26725157302078821fec7f37d.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=\" alt=\"\" /></p>\n<h2>为什么会堆内存溢出？</h2>\n<p>在年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时，JVM会对老年代进行完全的垃圾回收（Full GC）。如果GC后，还是无法存放从Survivor区复制过来的对象，就会出现OOM（Out of Memory）。</p>\n<p><strong>OOM（Out of Memory）异常常见有以下几个原因：</strong><br />1）老年代内存不足：java.lang.OutOfMemoryError:Javaheapspace<br />2）永久代内存不足：java.lang.OutOfMemoryError:PermGenspace<br />3）代码bug，占用内存无法及时回收。<br />OOM在这几个内存区都有可能出现，实际遇到OOM时，能根据异常信息定位到哪个区的内存溢出。<br />可以通过添加个参数-XX:+HeapDumpOnOutMemoryError，让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。</p>\n<p>熟悉了JAVA内存管理机制及配置参数，下面是对JAVA应用启动选项调优配置：</p>\n<pre class=\"has\"><code class=\"hljs php\"></code></pre>\n<ol class=\"hljs-ln\">\n<li>\n<div class=\"hljs-ln-numbers\">\n<div class=\"hljs-ln-line hljs-ln-n\" data-line-number=\"1\">&nbsp;</div>\n</div>\n<div class=\"hljs-ln-code\">\n<div class=\"hljs-ln-line\">JAVA_OPTS=<span class=\"hljs-string\">\"-server -Xms512m -Xmx2g -XX:+UseG1GC -XX:SurvivorRatio=6 -XX:MaxGCPauseMillis=400 -XX:G1ReservePercent=15 -XX:ParallelGCThreads=4 -XX:</span></div>\n</div>\n</li>\n<li>\n<div class=\"hljs-ln-numbers\">\n<div class=\"hljs-ln-line hljs-ln-n\" data-line-number=\"2\">&nbsp;</div>\n</div>\n<div class=\"hljs-ln-code\">\n<div class=\"hljs-ln-line\"><span class=\"hljs-string\">ConcGCThreads=1 -XX:InitiatingHeapOccupancyPercent=40 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:../logs/gc.log\"</span></div>\n</div>\n</li>\n</ol>\n<pre class=\"has\"><code class=\"hljs php\"></code></pre>\n<ul>\n<li>设置堆内存最小和最大值，最大值参考历史利用率设置</li>\n<li>设置GC垃圾收集器为G1</li>\n<li>启用GC日志，方便后期分析</li>\n</ul>\n<h2>小结</h2>\n<ul>\n<li>选择高效的GC算法，可有效减少停止应用线程时间。</li>\n<li>频繁Full GC会增加暂停时间和CPU使用率，可以加大老年代空间大小降低Full GC，但会增加回收时间，根据业务适当取舍。</li>\n</ul>\n<hr />\n<p><a href=\"https://blog.csdn.net/lingbo229/article/details/82586822\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n</div>', '“Java 虚拟机具有一个堆(Heap)，堆是运行时数据区域，所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 310, 5, '2019-12-07 19:32:24', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (53, '快速排序和三路快排', '<h1>快速排序</h1>\n<div class=\"para\">快速排序算法通过多次比较和交换来实现排序，其排序流程如下：</div>\n<div class=\"para\">(1)首先设定一个分界值，通过该分界值将数组分成左右两部分。</div>\n<div class=\"para\">(2)将大于或等于分界值的数据集中到数组右边，小于分界值的数据集中到数组的左边。此时，左边部分中各元素都小于或等于分界值，而右边部分中各元素都大于或等于分界值。</div>\n<div class=\"para\">(3)然后，左边和右边的数据可以独立排序。对于左侧的数组数据，又可以取一个分界值，将该部分数据分成左右两部分，同样在左边放置较小值，右边放置较大值。右侧的数组数据也可以做类似处理。</div>\n<div class=\"para\">(4)重复上述过程，可以看出，这是一个递归定义。通过递归将左侧部分排好序后，再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后，整个数组的排序也就完成了。</div>\n<pre class=\"language-java\"><code>public class quicksort {\n    public static void sort(int[] arry,int lo,int lt)\n    {\n        if(lo&lt;lt){\n        int t=partition(arry,lo,lt);\n        sort(arry,lo,t-1);\n        sort(arry,t+1,lt);\n        }\n\n    }\n\n    //快速排序的切分\n    private static int partition(int[] arry, int lo, int hi) {\n        int i=lo,j=hi+1;//左右扫描指针\n        int x=arry[lo];//切分元素\n        while(true){\n            //扫描左右，检查是否结束并交换元素\n            while(arry[++i]&lt;x){if(i==hi)break;}\n            while(arry[--j]&gt;x){if(j==lo)break;}\n            if(i&gt;=j)break;\n            swap(arry,i,j);\n\n        }\n        swap(arry,lo,j);//将x=arry[lo]放入正确的位置\n        return j;//a[lo..j-1]&lt;=a[j]&lt;=a[j+1..hi]达成\n    }\n\n    private static void swap(int[] arry, int i, int j) {\n        int x;\n        x=arry[j];\n        arry[j]=arry[i];\n        arry[i]=x;\n    }\n}</code></pre>\n<hr />\n<h1>三路快排</h1>\n<p>但是若序列中包含大量重复的元素，这种情况下，快排的性能就不那么理想了。</p>\n<p>下面开始学习三路快排的思想吧。</p>\n<h2>思路</h2>\n<p>将数组分为三部分，分别对应于小于、等于和大于哨兵元素tem的子序列。</p>\n<pre class=\"language-java\"><code>lo~le-1：未排序\nle~i-1：等于tem\ni~lt：未排序\nlt+1~hi:大于tem</code></pre>\n<pre class=\"language-java\"><code>    public void sort(int[] arr) {\n        backStack(arr, 0, arr.length-1);\n    }\n\n    public void backStack(int[] arr, int lo, int hi) {\n        if (lo &gt;= hi) {\n            return;\n        }\n        int i = lo + 1;\n        int le = lo;// lo-le-1 &lt;tem\n        int lt = hi;// lt-hi-1 &gt;tem\n        int tem = arr[lo];\n        while (i &lt;= lt) {\n            if (arr[i] &gt; tem) {\n                swap(arr, i, lt--);\n            } else if (arr[i] &lt; tem) {\n                swap(arr, i++, le++);\n            } else {\n                i++;\n            }\n        }\n        backStack(arr, lo, le - 1);\n        backStack(arr, lt + 1, hi);\n\n    }</code></pre>\n<p>&nbsp;</p>', '快速排序可以说是20世纪最伟大的算法之一了。相信都有所耳闻，它的速度也正如它的名字那样，是一个非常快的算法了。当然它也后期经过了不断的改进和优化，才被公认为是一个值得信任的非常优秀的算法。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 352, 3, '2019-12-08 12:33:31', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (54, '96. 不同的二叉搜索树', '<p>给定一个整数 n，求以&nbsp;1 ...&nbsp;n&nbsp;为节点组成的二叉搜索树有多少种？</p>\n<p>示例:</p>\n<p>输入: 3<br />输出: 5<br />解释:<br />给定 n = 3, 一共有 5 种不同结构的二叉搜索树:</p>\n<pre><img src=\"https://upload-images.jianshu.io/upload_images/5679451-7158f8ce8af4d528.png?imageMogr2/auto-orient/strip|imageView2/2/w/434/format/webp\" alt=\"\" width=\"434\" height=\"136\" /></pre>\n<hr />\n<pre>题解参考的官方答案</pre>\n<h3>动态规划<br />直觉</h3>\n<p>本问题可以用动态规划求解。</p>\n<p>给定一个有序序列 1 ... n，为了根据序列构建一棵二叉搜索树。我们可以遍历每个数字 i，将该数字作为树根，1 ... (i-1) 序列将成为左子树，(i+1) ... n 序列将成为右子树。于是，我们可以递归地从子序列构建子树。<br />在上述方法中，由于根各自不同，每棵二叉树都保证是独特的。</p>\n<p>可见，问题可以分解成规模较小的子问题。因此，我们可以存储并复用子问题的解，而不是递归的（也重复的）解决这些子问题，这就是动态规划法。</p>\n<p><strong>算法</strong></p>\n<p>问题是计算不同二叉搜索树的个数。为此，我们可以定义两个函数：</p>\n<ol>\n<li>\n<p><span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"mord mathdefault\">G</span><span class=\"mopen\">(</span><span class=\"mord mathdefault\">n</span><span class=\"mclose\">)</span></span></span></span>: 长度为<code>n</code>的序列的不同二叉搜索树个数。</p>\n</li>\n<li>\n<p><span class=\"katex\"><span class=\"katex-mathml\">F(i, n)</span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"mord mathdefault\">F</span><span class=\"mopen\">(</span><span class=\"mord mathdefault\">i</span><span class=\"mpunct\">,</span><span class=\"mord mathdefault\">n</span><span class=\"mclose\">)</span></span></span></span>: 以i为根的不同二叉搜索树个数(<span class=\"katex\"><span class=\"katex-mathml\">1 &lt;= i &lt;=n</span></span>)。</p>\n</li>\n</ol>\n<p>可见，</p>\n<blockquote>\n<p><span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"mord mathdefault\">G</span><span class=\"mopen\">(</span><span class=\"mord mathdefault\">n</span><span class=\"mclose\">)</span></span></span></span>&nbsp;是我们解决问题需要的函数。</p>\n</blockquote>\n<p>稍后我们将看到，G(n) 可以从 F(i, n) 得到，而 F(i, n)又会递归的依赖于G(n)。</p>\n<p>首先，根据上一节中的思路，不同的二叉搜索树的总数 G(n)，是对遍历所有 i (1 &lt;= i &lt;= n) 的 F(i, n) 之和。换而言之：</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p>特别的，对于边界情况，当序列长度为 1 （只有根）或为 0 （空树）时，只有一种情况。亦即：</p>\n<p><span class=\"base\"><span class=\"mord mathdefault\">G</span><span class=\"mopen\">(</span><span class=\"mord\">0</span><span class=\"mclose\">)</span><span class=\"mrel\">=</span></span><span class=\"base\"><span class=\"mord\">1</span><span class=\"mpunct\">,</span><span class=\"mord mathdefault\">G</span><span class=\"mopen\">(</span><span class=\"mord\">1</span><span class=\"mclose\">)</span><span class=\"mrel\">=</span></span><span class=\"base\"><span class=\"mord\">1</span></span></p>\n<p><span class=\"base\">给定序列&nbsp;<code>1 ... n</code>，我们选出数字&nbsp;<code>i</code>&nbsp;作为根，则对于根 i 的不同二叉搜索树数量&nbsp;<span class=\"katex\"><span class=\"katex-mathml\">F(i, n)</span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"mord mathdefault\">F</span><span class=\"mopen\">(</span><span class=\"mord mathdefault\">i</span><span class=\"mpunct\">,</span><span class=\"mord mathdefault\">n</span><span class=\"mclose\">)</span></span></span>，是左右子树个数的<strong>笛卡尔积</strong>，如下图所示:</span></p>\n<p><span class=\"base\"><img src=\"https://pic.leetcode-cn.com/fe9fb329250b328bb66032dda25b867e0047fcb480c2c0bcf14ecc2a4c12e454-image.png\" alt=\"\" width=\"700\" /></span></p>\n<p>举例而言，F(3, 7)F(3,7)，以 3 为根的不同二叉搜索树个数。为了以 3 为根从序列 [1, 2, 3, 4, 5, 6, 7] 构建二叉搜索树，我们需要从左子序列 [1, 2] 构建左子树，从右子序列 [4, 5, 6, 7] 构建右子树，然后将它们组合(即笛卡尔积)。<br />巧妙之处在于，我们可以将 [1,2] 构建不同左子树的数量表示为 G(2)G(2), 从 [4, 5, 6, 7]` 构建不同右子树的数量表示为 G(4)G(4)。这是由于 G(n)G(n) 和序列的内容无关，只和序列的长度有关。于是，F(3,7)=G(2)*G(4)。 概括而言，我们可以得到以下公式：</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p>将公式 (1)，(2) 结合，可以得到&nbsp;<span class=\"katex\"><span class=\"katex-mathml\">G(n)</span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"mord mathdefault\">G</span><span class=\"mopen\">(</span><span class=\"mord mathdefault\">n</span><span class=\"mclose\">)</span></span></span></span>&nbsp;的递归表达公式：</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p>为了计算函数结果，我们从小到大计算，因为 G(n) 的值依赖于G(0)&hellip;G(n&minus;1)。</p>\n<p>根据以上的分析和公式，很容易实现计算 G(n) 的算法。 下面是示例:</p>\n<pre class=\"language-java\"><code>public class Solution {\n  public int numTrees(int n) {\n    int[] G = new int[n + 1];\n    G[0] = 1;\n    G[1] = 1;\n\n    for (int i = 2; i &lt;= n; ++i) {\n      for (int j = 1; j &lt;= i; ++j) {\n        G[i] += G[j - 1] * G[i - j];\n      }\n    }\n    return G[n];\n  }\n}</code></pre>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<hr />\n<p><a href=\"https://leetcode-cn.com/problems/unique-binary-search-trees/solution/bu-tong-de-er-cha-sou-suo-shu-by-leetcode/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '96. 不同的二叉搜索树', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 245, 2, '2019-12-08 21:47:28', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (55, 'BIO,NIO,AIO区别', '<h2>BIO</h2>\n<p>Java BIO即Block I/O ， 同步并阻塞的IO。</p>\n<p>BIO 就是阻塞IO，每个TCP连接进来服务端都需要创建一个线程来建立连接并进行消息的处理。如果中间发生了阻塞(比如建立连接、读数据、写数据时发生阻碍)，线程也会发生阻塞，并发情况下，N个连接需要N个线程来处理。</p>\n<p><img src=\"https://img-blog.csdn.net/20170226131634860?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMDg1MzI2MQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast\" alt=\"\" width=\"1192\" height=\"514\" /></p>\n<h2>NIO</h2>\n<p>NIO是JDK1.4提出的，还是先用一段通俗的话来说明NIO的工作原理：</p>\n<p>NIO 也就是非阻塞IO，是基于事件驱动的思想(Reactor线程模型)。对比与BIO来说，NIO使用一个线程来管理所有的Socket 通道，也就是基于Selector机制，当查询到事件时(连接、接受连接、读、写)，就会转发给不同的处理线程(handler)。</p>\n<p>下面给出Reactor模型的工作应用图：</p>\n<p><img src=\"https://img-blog.csdn.net/20170226131734232?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMDg1MzI2MQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast\" alt=\"\" width=\"988\" height=\"488\" /></p>\n<h2>AIO</h2>\n<p>Java AIO即Async非阻塞，是异步非阻塞的IO。AIO是JDK1.7提出的，也就是异步IO</p>\n<p><a href=\"https://blog.csdn.net/u010853261/article/details/57409540\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n<hr />\n<h2>区别及联系</h2>\n<ul>\n<li>BIO （Blocking I/O）：同步阻塞I/O模式，数据的读取写入必须阻塞在一个线程内等待其完成。这里假设一个烧开水的场景，有一排水壶在烧开水，BIO的工作模式就是， 叫一个线程停留在一个水壶那，直到这个水壶烧开，才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。</li>\n<li>NIO （New I/O）：同时支持阻塞与非阻塞模式，但这里我们以其同步非阻塞I/O模式来说明，那么什么叫做同步非阻塞？如果还拿烧开水来说，NIO的做法是叫一个线程不断的轮询每个水壶的状态，看看是否有水壶的状态发生了改变，从而进行下一步的操作。</li>\n<li>AIO （ Asynchronous I/O）：异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里？异步非阻塞无需一个线程去轮询所有IO操作的状态改变，在相应的状态改变后，系统会通知对应的线程来处理。对应到烧开水中就是，为每个水壶上面装了一个开关，水烧开之后，水壶会自动通知我水烧开了。</li>\n</ul>\n<h2>各自适用场景</h2>\n<ul>\n<li>BIO方式适用于连接数目比较小且固定的架构，这种方式对服务器资源要求比较高，并发局限于应用中，JDK1.4以前的唯一选择，但程序直观简单易理解。</li>\n<li>NIO方式适用于连接数目多且连接比较短（轻操作）的架构，比如聊天服务器，并发局限于应用中，编程比较复杂，JDK1.4开始支持。</li>\n<li>AIO方式适用于连接数目多且连接比较长（重操作）的架构，比如相册服务器，充分调用OS参与并发操作，编程比较复杂，JDK7开始支持。</li>\n</ul>\n<hr />\n<h1>IO多路复用</h1>\n<div>\n<div>\n<p>IO多路复用是要和NIO一起使用的。尽管在操作系统级别，NIO和IO多路复用是两个相对独立的事情。NIO仅仅是指IO API总是能立刻返回，不会被Blocking；而IO多路复用仅仅是操作系统提供的一种便利的通知机制。操作系统并不会强制这俩必须得一起用&mdash;&mdash;你可以用NIO，但不用IO多路复用，就像上一节中的代码；也可以只用IO多路复用 + BIO，这时效果还是当前线程被卡住。但是，<strong>IO多路复用和NIO是要配合一起使用才有实际意义</strong>。</p>\n<p>对IO多路复用，还存在一些常见的误解，比如：</p>\n<ul>\n<li>\n<p><strong>❌IO多路复用是指多个数据流共享同一个Socket</strong>。其实IO多路复用说的是多个Socket，只不过操作系统是一起监听他们的事件而已。</p>\n</li>\n<li>\n<p><strong>❌IO多路复用是NIO，所以总是不Block的</strong>。其实IO多路复用的关键API调用(<code>select</code>，<code>poll</code>，<code>epoll_wait</code>）总是Block的，正如下文的例子所讲。</p>\n</li>\n<li>\n<p>❌<strong>IO多路复用和NIO一起减少了IO</strong>。实际上，IO本身（网络数据的收发）无论用不用IO多路复用和NIO，都没有变化。请求的数据该是多少还是多少；网络上该传输多少数据还是多少数据。IO多路复用和NIO一起仅仅是解决了调度的问题，避免CPU在这个过程中的浪费，使系统的瓶颈更容易触达到网络带宽，而非CPU或者内存。要提高IO吞吐，还是提高硬件的容量（例如，用支持更大带宽的网线、网卡和交换机）和依靠并发传输（例如HDFS的数据多副本并发传输）。</p>\n</li>\n</ul>\n<p>操作系统级别提供了一些接口来支持IO多路复用，最老掉牙的是<code>select</code>和<code>poll</code>。</p>\n</div>\n<div>\n<div>\n<p>但是目前来看，高性能的web服务器都不会使用<code>select</code>和<code>poll</code>。他们俩存在的意义仅仅是&ldquo;兼容性&rdquo;，因为很多操作系统都实现了这两个系统调用。</p>\n<p>如果是追求性能的话，在BSD/macOS上提供了kqueue api；在Salorias中提供了/dev/poll（可惜该操作系统已经凉凉)；而在Linux上提供了epoll api。它们的出现彻底解决了<code>select</code>和<code>poll</code>的问题。Java NIO，nginx等在对应的平台的上都是使用这些api实现。</p>\n<p>因为大部分情况下我会用Linux做服务器，所以下文以Linux epoll为例子来解释多路复用是怎么工作的。</p>\n<div>\n<div>\n<h1>用epoll实现的IO多路复用</h1>\n<p>epoll是Linux下的IO多路复用的实现。并且Linux也是目前最广泛被作为服务器的操作系统。细致的了解epoll对整个IO多路复用的工作原理非常有帮助。</p>\n</div>\n<div>\n<div>\n<p>为什么epoll的性能比<code>select</code>和<code>poll</code>要强呢？ <code>select</code>和<code>poll</code>每次都需要把完成的fd列表传入到内核，迫使内核每次必须从头扫描到尾。而epoll完全是反过来的。epoll在内核的数据被建立好了之后，每次某个被监听的fd一旦有事件发生，内核就直接标记之。<code>epoll_wait</code>调用时，会尝试直接读取到当时已经标记好的fd列表，如果没有就会进入等待状态。</p>\n<p>同时，<code>epoll_wait</code>直接只返回了被触发的fd列表，这样上层应用写起来也轻松愉快，再也不用从大量注册的fd中筛选出有事件的fd了。</p>\n<p>简单说就是<code>select</code>和<code>poll</code>的代价是<strong>\"O(所有注册事件fd的数量)\"</strong>，而epoll的代价是<strong>\"O(发生事件fd的数量)\"</strong>。于是，高性能网络服务器的场景特别适合用epoll来实现&mdash;&mdash;因为大多数网络服务器都有这样的模式：同时要监听大量（几千，几万，几十万甚至更多）的网络连接，但是短时间内发生的事件非常少。</p>\n<p>但是，假设发生事件的fd的数量接近所有注册事件fd的数量，那么epoll的优势就没有了，其性能表现会和<code>poll</code>和<code>select</code>差不多。</p>\n<p>epoll除了性能优势，还有一个优点&mdash;&mdash;同时支持水平触发(Level Trigger)和边沿触发(Edge Trigger)。</p>\n</div>\n</div>\n因为博主水平有限只是对网络上文章做了部分摘取，更多详细内容可以原文进行查看。</div>\n<p><a href=\"https://www.jianshu.com/p/ef418ccf2f7d\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n</div>\n</div>\n</div>', '在网络编程中，接触到最多的就是利用Socket进行网络通信开发。在Java中主要是以下三种实现方式BIO、NIO、AIO。关于这三个概念的辨析以前一直都是好像懂，但是表达的不是很清楚，下面做个总结完全辨析清楚。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 250, 6, '2019-12-09 21:01:11', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (56, 'Proxy.newProxyInstance', '<p>在java的动态代理机制中，有两个重要的类或接口，一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class)，这一个类和接口是实现我们动态代理所必须用到的。首先我们先来看看java的API帮助文档是怎么样对这两个类进行描述的：</p>\n<p>InvocationHandler：</p>\n<pre class=\"language-java\"><code>InvocationHandler is the interface implemented by the invocation handler of a proxy instance. \n \nEach proxy instance has an associated invocation handler. When a method is invoked on a proxy instance, the method invocation is encoded and dispatched to the invoke method of its invocation handler.</code></pre>\n<p>每一个动态代理类都必须要实现InvocationHandler这个接口，并且每个代理类的实例都关联到了一个handler，当我们通过代理对象调用一个方法的时候，这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。我们来看看InvocationHandler这个接口的唯一一个方法&nbsp;invoke&nbsp;方法：</p>\n<pre class=\"language-java\"><code>Object invoke(Object proxy, Method method, Object[] args) throws Throwable</code></pre>\n<p>我们看到这个方法一共接受三个参数，那么这三个参数分别代表什么呢？</p>\n<pre class=\"language-java\"><code>Object invoke(Object proxy, Method method, Object[] args) throws Throwable\nproxy:　 - 指代我们所代理的那个真实对象\nmethod:　- 指代的是我们所要调用真实对象的某个方法的Method对象\nargs:　　- 指代的是调用真实对象某个方法时接受的参数</code></pre>\n<p>如果不是很明白，等下通过一个实例会对这几个参数进行更深的讲解。</p>\n<p>接下来我们来看看 Proxy 这个类：</p>\n<pre class=\"language-java\"><code>Proxy provides static methods for creating dynamic proxy classes and instances, and it is also the superclass of all dynamic proxy classes created by those methods. </code></pre>\n<p>Proxy这个类的作用就是用来动态创建一个代理对象的类，它提供了许多的方法，但是我们用的最多的就是&nbsp;newProxyInstance&nbsp;这个方法：</p>\n<pre class=\"language-java\"><code>public static Object newProxyInstance(ClassLoader loader, Class&lt;?&gt;[] interfaces,  InvocationHandler h)  throws IllegalArgumentException</code></pre>\n<p>这个方法的作用就是得到一个动态的代理对象，其接收三个参数，我们来看看这三个参数所代表的含义：</p>\n<pre class=\"language-java\"><code>public static Object newProxyInstance(ClassLoader loader, Class&lt;?&gt;[] interfaces, InvocationHandler h) throws IllegalArgumentException\nloader:　　    一个ClassLoader对象，定义了由哪个ClassLoader对象来对生成的代理对象进行加载\ninterfaces:　　一个Interface对象的数组，表示的是我将要给我需要代理的对象提供一组什么接口，如果我提供了一组接口给它，那么这个代理对象就宣称实现了该接口(多态)，这样我就能调用这组接口中的方法了\nh:　　         一个InvocationHandler对象，表示的是当我这个动态代理对象在调用方法的时候，会关联到哪一个InvocationHandler对象上</code></pre>\n<hr />\n<p>好了，在介绍完这两个接口(类)以后，我们来通过一个实例来看看我们的动态代理模式是什么样的：</p>\n<p>首先我们定义了一个Subject类型的接口，为其声明了两个方法：</p>\n<pre class=\"language-java\"><code>public interface Subject\n{\n    public void rent();\n    \n    public void hello(String str);\n}</code></pre>\n<p>接着，定义了一个类来实现这个接口，这个类就是我们的真实对象，RealSubject类：</p>\n<pre class=\"language-java\"><code>public class RealSubject implements Subject\n{\n    @Override\n    public void rent()\n    {\n        System.out.println(\"I want to rent my house\");\n    }\n    \n    @Override\n    public void hello(String str)\n    {\n        System.out.println(\"hello: \" + str);\n    }\n}</code></pre>\n<p>下一步，我们就要定义一个动态代理类了，前面说个，每一个动态代理类都必须要实现 InvocationHandler 这个接口，因此我们这个动态代理类也不例外：</p>\n<pre class=\"language-java\"><code>public class DynamicProxy implements InvocationHandler\n{\n    // 这个就是我们要代理的真实对象\n    private Object subject;\n    \n    // 构造方法，给我们要代理的真实对象赋初值\n    public DynamicProxy(Object subject)\n    {\n        this.subject = subject;\n    }\n    \n    @Override\n    public Object invoke(Object object, Method method, Object[] args)\n            throws Throwable\n    {\n        // 在代理真实对象前我们可以添加一些自己的操作\n        System.out.println(\"before rent house\");\n        \n        System.out.println(\"Method:\" + method);\n        \n        // 当代理对象调用真实对象的方法时，其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用\n        method.invoke(subject, args);\n        \n        // 在代理真实对象后我们也可以添加一些自己的操作\n        System.out.println(\"after rent house\");\n        \n        return null;\n    }\n}</code></pre>\n<p>最后，来看看我们的Client类：</p>\n<pre class=\"language-java\"><code>public class Client\n{\n    public static void main(String[] args)\n    {\n        // 我们要代理的真实对象\n        Subject realSubject = new RealSubject();\n \n        // 我们要代理哪个真实对象，就将该对象传进去，最后是通过该真实对象来调用其方法的\n        InvocationHandler handler = new DynamicProxy(realSubject);\n \n        /*\n         * 通过Proxy的newProxyInstance方法来创建我们的代理对象，我们来看看其三个参数\n         * 第一个参数 handler.getClass().getClassLoader() ，我们这里使用handler这个类的ClassLoader对象来加载我们的代理对象\n         * 第二个参数realSubject.getClass().getInterfaces()，我们这里为代理对象提供的接口是真实对象所实行的接口，表示我要代理的是该真实对象，这样我就能调用这组接口中的方法了\n         * 第三个参数handler， 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上\n         */\n        Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject\n                .getClass().getInterfaces(), handler);\n        \n        System.out.println(subject.getClass().getName());\n        subject.rent();\n        subject.hello(\"world\");\n    }\n}</code></pre>\n<p>我们先来看看控制台的输出：</p>\n<pre class=\"language-java\"><code>$Proxy0\n \nbefore rent house\nMethod:public abstract void com.xiaoluo.dynamicproxy.Subject.rent()\nI want to rent my house\nafter rent house\n \nbefore rent house\nMethod:public abstract void com.xiaoluo.dynamicproxy.Subject.hello(java.lang.String)\nhello: world\nafter rent house</code></pre>\n<p><a href=\"https://blog.csdn.net/Dream_Weave/article/details/84183247\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '在学习Spring的时候，我们知道Spring主要有两大思想，一个是IoC，另一个就是AOP，对于IoC，而对于Spring的核心AOP来说，我们不但要知道怎么通过AOP来满足的我们的功能，我们更需要学习的是其底层是怎么样的一个原理，而AOP的原理就是java的动态代理机制。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 271, 6, '2019-12-10 18:03:23', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (57, 'ReadWriteLock', '<h2>基本讲解与使用</h2>\n<p>① ReadWriteLock同Lock一样也是一个接口，提供了readLock和writeLock两种锁的操作机制，一个是只读的锁，一个是写锁。</p>\n<p>读锁可以在没有写锁的时候被多个线程同时持有，写锁是独占的(排他的)。 每次只能有一个写线程，但是可以有多个线程并发地读数据。</p>\n<p>所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说，一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。</p>\n<p>理论上，读写锁比互斥锁允许对于共享数据更大程度的并发。与互斥锁相比，读写锁是否能够提高性能取决于读写数据的频率、读取和写入操作的持续时间、以及读线程和写线程之间的竞争。</p>\n<p>② 使用场景</p>\n<p>假设你的程序中涉及到对一些共享资源的读和写操作，且写操作没有读操作那么频繁。</p>\n<p>例如，最初填充有数据，然后很少修改的集合，同时频繁搜索（例如某种目录）是使用读写锁的理想候选项。</p>\n<p>在没有写操作的时候，两个线程同时读一个资源没有任何问题，所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源，就不应该再有其它线程对该资源进行读或写。这就需要一个读/写锁来解决这个问题。</p>\n<p>③ 互斥原则：</p>\n<p>读-读能共存，<br />读-写不能共存，<br />写-写不能共存。</p>\n<pre class=\"language-java\"><code>import java.util.concurrent.locks.ReadWriteLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\n\npublic class ReadWriteLockDemo {\n    private ReadWriteLock readWriteLock= new ReentrantReadWriteLock();\n    private int number=0;\n\n    public void get(){\n        readWriteLock.readLock().lock();\n        try{\n            System.out.println(Thread.currentThread().getName()+\"线程读取中。。\");\n            System.out.println(\"number:\"+number);\n        }finally {\n            readWriteLock.readLock().unlock();\n        }\n    }\n\n    public void set(Integer num){\n        readWriteLock.writeLock().lock();\n        try{\n            System.out.println(Thread.currentThread().getName()+\"线程写入中...........................................\");\n            Thread.sleep(1000);\n            this.number=num;\n            System.out.println(\"number:\"+number);\n            System.out.println(\"写入成功！\");\n            System.out.println();\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        } finally {\n            readWriteLock.writeLock().unlock();\n        }\n    }\n    public static void main(String[] args) throws InterruptedException {\n        //创建30个读线程，10个写线程\n        ReadWriteLockDemo readWriteLockDemo=new ReadWriteLockDemo();\n        for (int i = 0; i &lt; 30; i++) {\n            final int L=i;\n            new Thread(()-&gt;{\n                readWriteLockDemo.get();\n            },L+\"线程\").start();\n        }\n\n        for (int i = 40; i &lt; 50; i++) {\n            final int L=i;\n            new Thread(()-&gt;{\n                readWriteLockDemo.set(L);\n            },L+\"线程\").start();\n        }\n\n\n    }\n\n}\n</code></pre>\n<pre class=\"language-java\"><code>\n0线程线程读取中。。\nnumber:0\n1线程线程读取中。。\nnumber:0\n2线程线程读取中。。\nnumber:0\n4线程线程读取中。。\nnumber:0\n3线程线程读取中。。\nnumber:0\n6线程线程读取中。。\nnumber:0\n5线程线程读取中。。\n7线程线程读取中。。\nnumber:0\n8线程线程读取中。。\nnumber:0\n10线程线程读取中。。\nnumber:0\nnumber:0\n9线程线程读取中。。\nnumber:0\n11线程线程读取中。。\nnumber:0\n12线程线程读取中。。\nnumber:0\n13线程线程读取中。。\nnumber:0\n16线程线程读取中。。\nnumber:0\n15线程线程读取中。。\nnumber:0\n14线程线程读取中。。\nnumber:0\n17线程线程读取中。。\nnumber:0\n19线程线程读取中。。\nnumber:0\n18线程线程读取中。。\nnumber:0\n22线程线程读取中。。\nnumber:0\n20线程线程读取中。。\nnumber:0\n24线程线程读取中。。\nnumber:0\n26线程线程读取中。。\nnumber:0\n28线程线程读取中。。\nnumber:0\n40线程线程写入中...........................................\nnumber:40\n写入成功！\n\n21线程线程读取中。。\nnumber:40\n42线程线程写入中...........................................\nnumber:42\n写入成功！\n\n44线程线程写入中...........................................\nnumber:44\n写入成功！\n\n29线程线程读取中。。\nnumber:44\n46线程线程写入中...........................................\nnumber:46\n写入成功！\n\n43线程线程写入中...........................................\nnumber:43\n写入成功！\n\n25线程线程读取中。。\n23线程线程读取中。。\nnumber:43\nnumber:43\n47线程线程写入中...........................................\nnumber:47\n写入成功！\n\n27线程线程读取中。。\nnumber:47\n49线程线程写入中...........................................\nnumber:49\n写入成功！\n\n45线程线程写入中...........................................\nnumber:45\n写入成功！\n\n41线程线程写入中...........................................\nnumber:41\n写入成功！\n\n48线程线程写入中...........................................\nnumber:48\n写入成功！\n\n\n</code></pre>', 'ReadWriteLock同Lock一样也是一个接口，提供了readLock和writeLock两种锁的操作机制，一个是只读的锁，一个是写锁。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 330, 1, '2019-12-12 10:12:01', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (58, 'SQL JOINS', '<p>SQL语句查询的机器执行顺序：</p>\n<ol>\n<li>FROM</li>\n<li>ON</li>\n<li>JOIN</li>\n<li>WHERE</li>\n<li>GROUP BY</li>\n<li>CUBE|ROLLUP</li>\n<li>HAVING</li>\n<li>SELECT</li>\n<li>DISTINCT</li>\n<li>ORDER BY</li>\n<li>LIMIT</li>\n</ol>\n<hr />\n<p><img src=\"https://timgsa.baidu.com/timg?image&amp;quality=80&amp;size=b9999_10000&amp;sec=1576366392638&amp;di=21e7e36ce92143fc5524898e768b92ab&amp;imgtype=0&amp;src=http%3A%2F%2Fattach.dataguru.cn%2Fattachments%2Fforum%2F201304%2F09%2F210610grkvqb0i0rb3tobc.jpg\" alt=\"\" width=\"966\" height=\"760\" /></p>\n<p>Mysql不支持 FULL OUTER JOIN</p>\n<p>所以最后两种需使用 UNION</p>\n<pre class=\"language-markup\"><code>UNION 操作符用于合并两个或多个 SELECT 语句的结果集。\n\n请注意，UNION 内部的 SELECT 语句必须拥有相同数量的列。列也必须拥有相似的数据类型。同时，每条 SELECT 语句中的列的顺序必须相同。\n注释：默认地，UNION 操作符选取不同的值。如果允许重复的值，请使用 UNION ALL。</code></pre>\n<pre class=\"language-markup\"><code>select * from t1 left join t2 on t1.prid=t2.id\nunion\nselect * from t1 right join t2 on t1.prid=t2.id</code></pre>\n<pre class=\"language-markup\"><code>select * from t1 left join t2 on t1.prid=t2.id where t2.id is null\nunion\nselect * from t1 right join t2 on t1.prid=t2.id where t1.prid is null</code></pre>', 'SQL机器执行顺序，以及7种JOIN。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 291, 4, '2019-12-15 15:27:34', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (59, 'Redis持久化（RDB和AOF）', '<h3 id=\"item-1-2\">持久化的实现方式</h3>\n<h4><strong>快照方式持久化</strong></h4>\n<p>快照方式持久化就是在某时刻把所有数据进行完整备份。</p>\n<p>例：Mysql的Dump方式、<span style=\"color: #ff0000;\">Redis的RDB</span>方式。</p>\n<h4><strong>写日志方式持久化</strong></h4>\n<p>写日志方式持久化就是把用户执行的所有写指令（增删改）备份到文件中，还原数据时只需要把备份的所有指令重新执行一遍即可。</p>\n<p>例：Mysql的Binlog、<span style=\"color: #ff0000;\">Redis的AOF</span>、Hbase的HLog。</p>\n<hr />\n<h2 id=\"item-2\">RDB</h2>\n<p><img src=\"https://image-static.segmentfault.com/200/180/2001805000-5b73cfc79b2d5_articlex\" alt=\"\" /></p>\n<p>RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储。<br />在默认情况下， Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。<br />在 Redis 运行时， RDB 程序将当前内存中的数据库快照保存到磁盘文件中， 在 Redis 重启动时， RDB 程序可以通过载入 RDB 文件来还原数据库的状态。</p>\n<h3><strong>工作方式</strong></h3>\n<p>当 Redis 需要保存 dump.rdb 文件时， 服务器执行以下操作:</p>\n<ol>\n<li>Redis 调用forks。同时拥有父进程和子进程。</li>\n<li>子进程将数据集写入到一个临时 RDB 文件中。</li>\n<li>当子进程完成对新 RDB 文件的写入时，Redis 用新 RDB 文件替换原来的 RDB 文件，并删除旧的 RDB 文件。</li>\n</ol>\n<p>这种工作方式使得 Redis 可以从写时复制（copy-on-write）机制中获益。</p>\n<h3 id=\"item-2-4\">RDB的三种主要触发机制</h3>\n<h4><strong>save命令（同步数据到磁盘上）</strong></h4>\n<p><code>save</code>&nbsp;命令执行一个同步操作，以RDB文件的方式保存所有数据的快照。</p>\n<pre class=\"hljs css\"><code>127<span class=\"hljs-selector-class\">.0</span><span class=\"hljs-selector-class\">.0</span><span class=\"hljs-selector-class\">.1</span><span class=\"hljs-selector-pseudo\">:6379</span>&gt; <span class=\"hljs-selector-tag\">save</span>\n<span class=\"hljs-selector-tag\">OK</span></code><br /><br /><img src=\"https://image-static.segmentfault.com/381/183/381183997-5b73cfc78f7e3_articlex\" alt=\"\" /><br /><br />由于&nbsp;<span style=\"color: #ff0000;\"><code>save</code>&nbsp;命令是同步命令，会占用Redis的主进程</span>。若Redis数据非常多时，<code>save</code>命令执行速度会非常慢，阻塞所有客户端的请求。<br />因此很少在生产环境直接使用SAVE 命令，可以使用BGSAVE 命令代替。如果在BGSAVE命令的保存数据的子进程发生错误的时，用 SAVE命令保存最新的数据是最后的手段。<br /><img src=\"https://image-static.segmentfault.com/124/101/1241012198-5b73cfc7710b8_articlex\" alt=\"\" /></pre>\n<h4><strong>bgsave命令（异步保存数据到磁盘上）</strong></h4>\n<p><span style=\"color: #ff0000;\"><code>bgsave</code>&nbsp;命令执行一个异步操作，以RDB文件的方式保存所有数据的快照。</span></p>\n<pre class=\"hljs css\"><code>127<span class=\"hljs-selector-class\">.0</span><span class=\"hljs-selector-class\">.0</span><span class=\"hljs-selector-class\">.1</span><span class=\"hljs-selector-pseudo\">:6379</span>&gt; <span class=\"hljs-selector-tag\">bgsave</span>\n<span class=\"hljs-selector-tag\">Background</span> <span class=\"hljs-selector-tag\">saving</span> <span class=\"hljs-selector-tag\">started</span>\n</code></pre>\n<p>Redis使用Linux系统的<code>fock()</code>生成一个子进程来将DB数据保存到磁盘，主进程继续提供服务以供客户端调用。<br />如果操作成功，可以通过客户端命令LASTSAVE来检查操作结果。</p>\n<p><img src=\"https://image-static.segmentfault.com/602/904/602904611-5b73cfc740cfe_articlex\" alt=\"\" /></p>\n<h4><strong><code>save</code>&nbsp;与&nbsp;<code>bgsave</code>&nbsp;对比</strong></h4>\n<table>\n<thead>\n<tr>\n<th>命令</th>\n<th>save</th>\n<th>bgsave</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>IO类型</td>\n<td>同步</td>\n<td>异步</td>\n</tr>\n<tr>\n<td>阻塞？</td>\n<td>是</td>\n<td>是（阻塞发生在fock()，通常非常快）</td>\n</tr>\n<tr>\n<td>复杂度</td>\n<td>O(n)</td>\n<td>O(n)</td>\n</tr>\n<tr>\n<td>优点</td>\n<td>不会消耗额外的内存</td>\n<td>不阻塞客户端命令</td>\n</tr>\n<tr>\n<td>缺点</td>\n<td>阻塞客户端命令</td>\n<td>需要fock子进程，消耗内存</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"item-2-5\">RDB相关配置</h3>\n<pre class=\"language-markup\"><code># RDB自动持久化规则\n# 当 900 秒内有至少有 1 个键被改动时，自动进行数据集保存操作\nsave 900 1\n# 当 300 秒内有至少有 10 个键被改动时，自动进行数据集保存操作\nsave 300 10\n# 当 60 秒内有至少有 10000 个键被改动时，自动进行数据集保存操作\nsave 60 10000\n\n# RDB持久化文件名\ndbfilename dump-&lt;port&gt;.rdb\n\n# 数据持久化文件存储目录\ndir /var/lib/redis\n\n# bgsave发生错误时是否停止写入，通常为yes\nstop-writes-on-bgsave-error yes\n\n# rdb文件是否使用压缩格式\nrdbcompression yes\n\n# 是否对rdb文件进行校验和检验，通常为yes\nrdbchecksum yes</code></pre>\n<h3 id=\"item-2-6\">RDB的优点</h3>\n<ol>\n<li>RDB是一个非常紧凑的文件，它保存了某个时间点得数据集，非常适用于数据集的备份，比如你可以在每个小时报保存一下过去24小时内的数据，同时每天保存过去30天的数据，这样即使出了问题你也可以根据需求恢复到不同版本的数据集。</li>\n<li>RDB是一个紧凑的单一文件，很方便传送到另一个远端数据中心或者亚马逊的S3（可能加密），非常适用于灾难恢复。</li>\n<li>RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程，接下来的工作全部由子进程来做，父进程不需要再做其他IO操作，所以RDB持久化方式可以最大化redis的性能。</li>\n<li>与AOF相比，在恢复大的数据集的时候，RDB方式会更快一些。</li>\n</ol>\n<h3 id=\"item-2-7\">RDB的缺点</h3>\n<ol>\n<li>耗时、耗性能。RDB 需要经常fork子进程来保存数据集到硬盘上，当数据集比较大的时候，fork的过程是非常耗时的，可能会导致Redis在一些毫秒级内不能响应客户端的请求。如果数据集巨大并且CPU性能不是很好的情况下，这种情况会持续1秒，AOF也需要fork，但是你可以调节重写日志文件的频率来提高数据集的耐久度。</li>\n<li>不可控、丢失数据。如果你希望在redis意外停止工作（例如电源中断）的情况下丢失的数据最少的话，那么RDB不适合你。虽然你可以配置不同的save时间点(例如每隔5分钟并且对数据集有100个写的操作)，是Redis要完整的保存整个数据集是一个比较繁重的工作，你通常会每隔5分钟或者更久做一次完整的保存，万一在Redis意外宕机，你可能会丢失几分钟的数据。</li>\n</ol>\n<hr />\n<h2 id=\"item-3\">AOF</h2>\n<p>打开AOF后， <span style=\"color: #ff0000;\">每当 Redis 执行一个改变数据集的命令时（比如 SET）， 这个命令就会被追加到 AOF 文件的末尾。</span>这样的话， 当 Redis 重新启时， 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。</p>\n<h4><strong>AOF运行原理 - 创建</strong></h4>\n<p><strong><img src=\"https://image-static.segmentfault.com/351/968/351968413-5b73cfc7205e7_articlex\" alt=\"\" /></strong></p>\n<h4><strong>AOF运行原理 - 恢复</strong></h4>\n<p><strong><img src=\"https://image-static.segmentfault.com/115/755/1157551996-5b73cfc79cdb5_articlex\" alt=\"\" /></strong></p>\n<h3 id=\"item-3-9\">AOF持久化的三种策略</h3>\n<p>你可以通过配置文件配置 Redis 多久才将数据 fsync 到磁盘一次。</p>\n<h4><strong>always</strong></h4>\n<p>每次有新命令追加到 AOF 文件时就执行一次 fsync ：非常慢，也非常安全。</p>\n<p><img src=\"https://image-static.segmentfault.com/125/754/1257547519-5b73cfc79c755_articlex\" alt=\"\" /></p>\n<h4><strong>everysec</strong></h4>\n<p>每秒 fsync 一次：足够快（和使用 RDB 持久化差不多），并且在故障时只会丢失 1 秒钟的数据。<br />推荐（并且也是默认）的措施为每秒 fsync 一次， 这种 fsync 策略可以兼顾速度和安全性。</p>\n<p><img src=\"https://image-static.segmentfault.com/948/114/948114448-5b73cfc6d59bc_articlex\" alt=\"\" /></p>\n<h4><strong>no</strong></h4>\n<p>从不 fsync ：将数据交给操作系统来处理，由操作系统来决定什么时候同步数据。更快，也更不安全的选择。</p>\n<p><img src=\"https://image-static.segmentfault.com/122/826/1228264883-5b73cfc6d23f9_articlex\" alt=\"\" /></p>\n<h4><strong>always、everysec、no对比</strong></h4>\n<table>\n<thead>\n<tr>\n<th>命令</th>\n<th>优点</th>\n<th>缺点</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>always</td>\n<td>不丢失数据</td>\n<td>IO开销大，一般SATA磁盘只有几百TPS</td>\n</tr>\n<tr>\n<td>everysec</td>\n<td>每秒进行与fsync，最多丢失1秒数据</td>\n<td>可能丢失1秒数据</td>\n</tr>\n<tr>\n<td>no</td>\n<td>不用管</td>\n<td>不可控</td>\n</tr>\n</tbody>\n</table>\n<p>推荐（并且也是默认）的措施为每秒 fsync 一次， 这种 fsync 策略可以兼顾速度和安全性。</p>\n<h3 id=\"item-3-10\">AOF重写</h3>\n<p>因为 AOF 的运作方式是不断地将命令追加到文件的末尾， 所以随着写入命令的不断增加， AOF 文件的体积也会变得越来越大。举个例子， 如果你对一个计数器调用了 100 次 INCR ， 那么仅仅是为了保存这个计数器的当前值， AOF 文件就需要使用 100 条记录（entry）。然而在实际上， 只使用一条 SET 命令已经足以保存计数器的当前值了， 其余 99 条记录实际上都是多余的。<br />为了处理这种情况， Redis 支持一种有趣的特性： 可以在不打断服务客户端的情况下， 对 AOF 文件进行重建（rebuild）。执行 bgrewriteaof 命令， Redis 将生成一个新的 AOF 文件， 这个文件包含重建当前数据集所需的最少命令。<br />Redis 2.2 需要自己手动执行 bgrewriteaof 命令； Redis 2.4 则可以通过配置自动触发 AOF 重写。</p>\n<p><img src=\"https://image-static.segmentfault.com/181/263/1812631962-5b73cfc6d572d_articlex\" alt=\"\" /></p>\n<h4><strong>AOF重写的作用</strong></h4>\n<ul>\n<li>减少磁盘占用量</li>\n<li>加速数据恢复</li>\n</ul>\n<h4><strong>AOF重写的实现方式</strong></h4>\n<ul>\n<li>\n<p><strong>bgrewriteaof 命令</strong></p>\n<p>Redis bgrewriteaof 命令用于异步执行一个 AOF（AppendOnly File）文件重写操作。重写会创建一个当前AOF文件的体积优化版本。<br />即使 bgrewriteaof 执行失败，也不会有任何数据丢失，因为旧的AOF文件在 bgrewriteaof 成功之前不会被修改。<br />AOF 重写由 Redis 自行触发，bgrewriteaof 仅仅用于手动触发重写操作。<br />具体内容:</p>\n<ul>\n<li>如果一个子Redis是通过磁盘快照创建的，AOF重写将会在RDB终止后才开始保存。这种情况下BGREWRITEAOF任然会返回OK状态码。从Redis 2.6起你可以通过INFO命令查看AOF重写执行情况。</li>\n<li>如果只在执行的AOF重写返回一个错误，AOF重写将会在稍后一点的时间重新调用。</li>\n</ul>\n</li>\n</ul>\n<p><img src=\"https://image-static.segmentfault.com/360/676/3606763245-5b73cfc6d009c_articlex\" alt=\"\" /></p>\n<ul>\n<li><strong>AOF重写配置</strong></li>\n</ul>\n<table>\n<thead>\n<tr>\n<th>配置名</th>\n<th>含义</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>auto-aof-rewrite-min-size</td>\n<td>触发AOF文件执行重写的最小尺寸</td>\n</tr>\n<tr>\n<td>auto-aof-rewrite-percentage</td>\n<td>触发AOF文件执行重写的增长率</td>\n</tr>\n</tbody>\n</table>\n<table>\n<thead>\n<tr>\n<th>统计名</th>\n<th>含义</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>aof_current_size</td>\n<td>AOF文件当前尺寸（字节）</td>\n</tr>\n<tr>\n<td>aof_base_size</td>\n<td>AOF文件上次启动和重写时的尺寸（字节）</td>\n</tr>\n</tbody>\n</table>\n<blockquote>\n<p>AOF重写自动触发机制，需要同时满足下面两个条件：</p>\n<ul>\n<li>aof_current_size &gt; auto-aof-rewrite-min-size</li>\n<li>(aof_current_size - aof_base_size) * 100 / aof_base_size &gt; auto-aof-rewrite-percentage</li>\n</ul>\n</blockquote>\n<p>假设 Redis 的配置项为：</p>\n<pre class=\"hljs arduino\"><code><span class=\"hljs-keyword\">auto</span>-aof-rewrite-<span class=\"hljs-built_in\">min</span>-<span class=\"hljs-built_in\">size</span> <span class=\"hljs-number\">64</span>mb\n<span class=\"hljs-keyword\">auto</span>-aof-rewrite-percentage <span class=\"hljs-number\">100</span></code></pre>\n<p>当AOF文件的体积大于64Mb，并且AOF文件的体积比上一次重写之久的体积大了至少一倍（100%）时，Redis将执行 bgrewriteaof 命令进行重写。</p>\n<h3 id=\"item-3-11\">AOF相关配置</h3>\n<pre class=\"language-markup\"><code># 开启AOF持久化方式\nappendonly yes\n\n# AOF持久化文件名\nappendfilename appendonly-&lt;port&gt;.aof\n\n# 每秒把缓冲区的数据同步到磁盘\nappendfsync everysec\n\n# 数据持久化文件存储目录\ndir /var/lib/redis\n\n# 是否在执行重写时不同步数据到AOF文件\n# 这里的 yes，就是执行重写时不同步数据到AOF文件\nno-appendfsync-on-rewrite yes\n\n# 触发AOF文件执行重写的最小尺寸\nauto-aof-rewrite-min-size 64mb\n\n# 触发AOF文件执行重写的增长率\nauto-aof-rewrite-percentage 100</code></pre>\n<h3 id=\"item-3-12\">AOF的优点</h3>\n<ol>\n<li>使用AOF 会让你的Redis更加耐久: 你可以使用不同的fsync策略：无fsync，每秒fsync，每次写的时候fsync。使用默认的每秒fsync策略，Redis的性能依然很好(fsync是由后台线程进行处理的，主线程会尽力处理客户端请求)，一旦出现故障，你最多丢失1秒的数据。</li>\n<li>AOF文件是一个只进行追加的日志文件，所以不需要写入seek，即使由于某些原因(磁盘空间已满，写的过程中宕机等等)未执行完整的写入命令，你也也可使用redis-check-aof工具修复这些问题。</li>\n<li>Redis 可以在 AOF 文件体积变得过大时，自动地在后台对 AOF 进行重写： 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的，因为 Redis 在创建新 AOF 文件的过程中，会继续将命令追加到现有的 AOF 文件里面，即使重写过程中发生停机，现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕，Redis 就会从旧 AOF 文件切换到新 AOF 文件，并开始对新 AOF 文件进行追加操作。</li>\n<li>AOF 文件有序地保存了对数据库执行的所有写入操作， 这些写入操作以 Redis 协议的格式保存， 因此 AOF 文件的内容非常容易被人读懂， 对文件进行分析（parse）也很轻松。 导出（export） AOF 文件也非常简单： 举个例子， 如果你不小心执行了 FLUSHALL 命令， 但只要 AOF 文件未被重写， 那么只要停止服务器， 移除 AOF 文件末尾的 FLUSHALL 命令， 并重启 Redis ， 就可以将数据集恢复到 FLUSHALL 执行之前的状态。</li>\n</ol>\n<h3 id=\"item-3-13\">AOF的缺点</h3>\n<ol>\n<li>对于相同的数据集来说，AOF 文件的体积通常要大于 RDB 文件的体积。</li>\n<li>根据所使用的 fsync 策略，AOF 的速度可能会慢于 RDB 。 在一般情况下， 每秒 fsync 的性能依然非常高， 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快， 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时，RDB 可以提供更有保证的最大延迟时间（latency）。</li>\n</ol>\n<hr />\n<h3 id=\"item-4-14\">RDB 和 AOF 对比</h3>\n<table>\n<thead>\n<tr>\n<th>-</th>\n<th>RDB</th>\n<th>AOF</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>启动优先级</td>\n<td>低</td>\n<td>高</td>\n</tr>\n<tr>\n<td>体积</td>\n<td>小</td>\n<td>大</td>\n</tr>\n<tr>\n<td>恢复速度</td>\n<td>快</td>\n<td>慢</td>\n</tr>\n<tr>\n<td>数据安全性</td>\n<td>丢数据</td>\n<td>根据策略决定</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"item-4-15\">如何选择使用哪种持久化方式？</h3>\n<p>一般来说， 如果想达到足以媲美 PostgreSQL 的数据安全性， 你应该同时使用两种持久化功能。</p>\n<p>如果你非常关心你的数据， 但仍然可以承受数分钟以内的数据丢失， 那么你可以只使用 RDB 持久化。</p>\n<p>有很多用户都只使用 AOF 持久化， 但并不推荐这种方式： 因为定时生成 RDB 快照（snapshot）非常便于进行数据库备份， 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。</p>\n<p><a href=\"https://segmentfault.com/a/1190000016021217\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '持久化Redis所有数据保持在内存中，对数据的更新将异步地保存到磁盘上。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 335, 4, '2019-12-17 18:20:06', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (60, '178. 分数排名', '<p>编写一个 SQL 查询来实现分数排名。如果两个分数相同，则两个分数排名（Rank）相同。请注意，平分后的下一个名次应该是下一个连续的整数值。换句话说，名次之间不应该有&ldquo;间隔&rdquo;。</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<hr />\n<p>【解题思路】</p>\n<p>1.涉及到排名问题，可以使用窗口函数</p>\n<p>2.专用窗口函数rank, dense_rank, row_number有什么区别呢？</p>\n<p>它们的区别我举个例子，你们一下就能看懂：</p>\n<pre class=\"language-markup\"><code>select *,\n   rank() over (order by 成绩 desc) as ranking,\n   dense_rank() over (order by 成绩 desc) as dese_rank,\n   row_number() over (order by 成绩 desc) as row_num\nfrom 班级\n</code></pre>\n<p><img src=\"https://pic.leetcode-cn.com/555db2ac6d57cc9c591c6475de79262f7ba4ecd43142ff0750e09d4d18fdffa6-1.png\" alt=\"\" width=\"635\" height=\"476\" /></p>\n<p>从上面的结果可以看出：<br />1）rank函数：这个例子中是5位，5位，5位，8位，也就是如果有并列名次的行，会占用下一名次的位置。比如正常排名是1，2，3，4，但是现在前3名是并列的名次，结果是：1，1，1，4。</p>\n<p>2）dense_rank函数：这个例子中是5位，5位，5位，6位，也就是如果有并列名次的行，不占用下一名次的位置。比如正常排名是1，2，3，4，但是现在前3名是并列的名次，结果是：1，1，1，2。</p>\n<p>3）row_number函数：这个例子中是5位，6位，7位，8位，也就是不考虑并列名次的情况。比如前3名是并列的名次，排名是正常的1，2，3，4。</p>\n<p>这三个函数的区别如下：</p>\n<p><img src=\"https://pic.leetcode-cn.com/729cc8ee48f55e4c4c448d764e6c0c1e1de50a7cb1674fd557abff50519651a8-1.png\" alt=\"\" width=\"636\" height=\"476\" /></p>\n<p>根据题目要求的排名规则，这里我们使用dense_rank函数。</p>\n<hr />\n<p><a href=\"https://leetcode-cn.com/problems/rank-scores/solution/tu-jie-sqlmian-shi-ti-jing-dian-pai-ming-wen-ti-by/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '编写一个 SQL 查询来实现分数排名。如果两个分数相同，则两个分数排名（Rank）相同。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 346, 2, '2019-12-20 17:03:18', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (61, 'ActiveMq入门', '<h2>JMS&nbsp;</h2>\n<p>Java 消息中间件的服务接口规范，activemq 之上是 mq ， 而 mq 之上是JMS 定义的消息规范 。 activemq 是mq 技术的一种理论实现（与之相类似的实现还有 Kafka RabbitMQ RockitMQ ），而 JMS 是更上一级的规范。</p>\n<p><img src=\"https://note.youdao.com/yws/public/resource/39fb9a2ff543d021d619885806a15f05/xmlnote/ACE9B20C3FA74BB581858A7C58FF8630/12122\" alt=\"\" width=\"972\" height=\"543\" /></p>\n<div>在点对点的消息传递时，目的地称为 队列 queue</div>\n<div>在发布订阅消息传递中，目的地称为 主题 topic</div>\n<hr />\n<p>ActiveMQ的安装与启动</p>\n<p>（1）官网下载对应服务器版本</p>\n<p>（2）解压后进入apache-activemq-5.15.9/bin目录</p>\n<p>（3）执行./activemq start启动ActiveMQ</p>\n<p>（4）浏览器输入ActiveMQ启动的服务器ip:8161便可进入web界面，点击Manage ActiveMQ broker可以查看消息推送的状态，默认账号密码为admin,admin</p>\n<hr />\n<h2>测试 queue Demo</h2>\n<ul>\n<li>生产者，生产5条消息</li>\n</ul>\n<pre class=\"language-markup\"><code>   &lt;!--Maven依赖--&gt;\n    &lt;dependency&gt;\n        &lt;groupId&gt;org.apache.activemq&lt;/groupId&gt;\n        &lt;artifactId&gt;activemq-all&lt;/artifactId&gt;\n        &lt;version&gt;5.9.0&lt;/version&gt;\n    &lt;/dependency&gt;</code></pre>\n<pre class=\"language-java\"><code>import org.apache.activemq.ActiveMQConnectionFactory;\n\nimport javax.jms.*;\n\npublic class MyProducer {\n    private static final String ACTIVEMQ_URL = \"tcp://localhost:61616\";\n    private static final String Que_Name=\"queue01\";\n    public static void main(String[] args) throws JMSException {\n        //1 create factory\n        ActiveMQConnectionFactory activeMQConnectionFactory=new ActiveMQConnectionFactory(ACTIVEMQ_URL);\n        //2 create connection\n        Connection connection = activeMQConnectionFactory.createConnection();\n        //3 create session\n        connection.start();\n        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);\n        //4 queue or topic\n        Queue queue = session.createQueue(Que_Name);\n        //5 create producer\n        MessageProducer producer = session.createProducer(queue);\n        //6 create message\n        for (int i = 0; i &lt; 5; i++) {\n            //创建文本消息\n            TextMessage message = session.createTextMessage(\"第\" + i + \"个文本消息\");\n            //发送消息\n            producer.send(message);\n            System.out.println(message.getText());\n\n        }\n        producer.close();\n        session.close();\n        connection.close();\n\n\n    }\n}\n</code></pre>\n<p>测试查看web后台显示，有5条消息在队列中等待消费</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<ul>\n<li>消费者</li>\n</ul>\n<pre class=\"language-java\"><code>import org.apache.activemq.ActiveMQConnectionFactory;\n\nimport javax.jms.*;\n\npublic class MyConsumer {\n    private static final String ACTIVEMQ_URL = \"tcp://localhost:61616\";\n    private static final String Que_Name=\"queue01\";\n\n    public static void main(String[] args) throws JMSException {\n        // 创建连接工厂\n        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);\n        // 创建连接\n        Connection connection = activeMQConnectionFactory.createConnection();\n        // 打开连接\n        connection.start();\n        // 创建会话\n        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);\n        // 创建队列目标,并标识队列名称，消费者根据队列名称接收数据\n        Destination destination = session.createQueue(Que_Name);\n        // 创建消费者\n        MessageConsumer consumer = session.createConsumer(destination);\n        // 创建消费的监听\n        consumer.setMessageListener(new MessageListener() {\n            public void onMessage(Message message) {\n                TextMessage textMessage = (TextMessage) message;\n                try {\n                    System.out.println(\"消费的消息：\" + textMessage.getText());\n                } catch (JMSException e) {\n                    e.printStackTrace();\n                }\n            }\n        });\n    }\n}\n</code></pre>\n<p>测试查看web后台显示，5条消息被消费</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p>注意： activemq 自带负载均衡，当先启动两个队列（Queue）的消费者时，在启动生产者发出消息，此时的消息平均的被两个消费者消费。 并且消费者不会消费已经被消费的消息（即为已经出队的消息）</p>\n<hr />\n<h2>Topic模式</h2>\n<p>但是当有多个主题（Topic）订阅者时，发布者发布的消息，每个订阅者都会接收所有的消息。topic 更像是被广播的消息，但是缺点是不能接受已经发送过的消息。</p>\n<p><img src=\"https://note.youdao.com/yws/public/resource/39fb9a2ff543d021d619885806a15f05/xmlnote/2A7A19CFE5054A8AB948376B47DE97DE/12217\" alt=\"\" width=\"810\" height=\"485\" /></p>\n<p><span style=\"color: #ff0000;\">topic模式，先要有订阅者，生产者才有意义。</span></p>\n<hr />\n<h2>保证消息的可靠性</h2>\n<h3>持久化</h3>\n<pre class=\"language-markup\"><code>// 在队列为目的地的时候持久化消息\nmessageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);\n\n// 队列为目的地的非持久化消息\nmessageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);</code></pre>\n<div>持久化的消息，服务器宕机后消息依旧存在，只是没有入队，当服务器再次启动，消息任就会被消费。</div>\n<div>但是非持久化的消息，服务器宕机后消息永远丢失。 而当你没有注明是否是持久化还是非持久化时，默认是持久化的消息。</div>\n<div>&nbsp;</div>\n<div>对于目的地为主题（topic）来说，默认就是非持久化的，让主题的订阅支持化的意义在于：对于订阅了公众号的人来说，当用户手机关机，在开机后任就可以接受到关注公众号之前发送的消息。</div>\n<div>&nbsp;</div>\n<div>\n<div>对于目的地为主题（topic）来说，默认就是非持久化的，让主题的订阅支持化的意义在于：对于订阅了公众号的人来说，当用户手机关机，在开机后任就可以接受到关注公众号之前发送的消息。</div>\n<div>代码实现：持久化topic 的消费者\n<pre class=\"language-java\"><code>     &hellip;&hellip;    // 前面代码相同，不复制了      \n        Topic topic = session.createTopic(TOPIC_NAME);\n        TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic,\"remark...\");\n\n         //发布订阅\n        connection.start();\n\n        Message message = topicSubscriber.receive();// 一直等\n         while (null != message){\n             TextMessage textMessage = (TextMessage)message;\n             System.out.println(\" 收到的持久化 topic ：\"+textMessage.getText());\n             message = topicSubscriber.receive(3000L);    // 等1秒后meesage 为空，跳出循环，控制台关闭\n         }\n   &hellip;&hellip;​</code></pre>\n<div>持久化生产者</div>\n<div id=\"7510-1563505726307\" class=\"block-view code-view yne-code-theme-default\" data-language=\"javascript\" data-theme=\"default\">\n<div class=\"para-text\">\n<pre class=\"language-java\"><code>  &hellip;&hellip;  \n   \n        MessageProducer messageProducer = session.createProducer(topic);\n        // 6 通过messageProducer 生产 3 条 消息发送到消息队列中\n        // 设置持久化topic 在启动\n        messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT); \n        connection.start();\n        for (int i = 1; i &lt; 4 ; i++) {\n            // 7  创建字消息\n            TextMessage textMessage = session.createTextMessage(\"topic_name--\" + i);\n            // 8  通过messageProducer发布消息\n            messageProducer.send(textMessage);\n\n            MapMessage mapMessage = session.createMapMessage();\n            //    mapMessage.setString(\"k1\",\"v1\");\n            //     messageProducer.send(mapMessage);\n        }\n        // 9 关闭资源\n      &hellip;&hellip; </code></pre>\n<h3>事务</h3>\n<div>createSession的第一个参数为true 为开启事务，开启事务之后必须在将消息提交，才可以在队列中看到消息</div>\n<div id=\"9054-1563507508363\" class=\"block-view code-view yne-code-theme-default\" data-language=\"javascript\" data-theme=\"default\">\n<div class=\"para-text\">\n<pre class=\"language-java\"><code>Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);</code></pre>\n</div>\n</div>\n<div>提交：</div>\n<div id=\"6012-1563507584294\" class=\"block-view code-view yne-code-theme-default\" data-language=\"javascript\" data-theme=\"default\">\n<div class=\"para-text\">\n<pre class=\"language-java\"><code>session.commit(); </code></pre>\n<div>事务开启的意义在于，如果对于多条必须同批次传输的消息，可以使用事务，如果一条传输失败，可以将事务回滚，再次传输，保证数据的完整性。</div>\n<div>对于消息消费者来说，开启事务的话，可以避免消息被多次消费，以及后台和服务器数据的不一致性。举个栗子：</div>\n<div>如果消息消费的 createSession 设置为 ture ，但是没有 commit ，此时就会造成非常严重的后果，那就是在后台看来消息已经被消费，但是对于服务器来说并没有接收到消息被消费，此时就有可能被多次消费。</div>\n<h3>Acknowledge 签收 （俗称ack）</h3>\n<p>事务主要针对生产者，签收主要针对消费者。</p>\n<pre class=\"language-java\"><code>Session.AUTO_ACKNOWLEDGE      自动签收，默认\n\nSession.CLIENT_ACKNOWLEDGE     手动签收\n手动签收需要acknowledge   \ntextMessage.acknowledge();</code></pre>\n<p>对于开启事务时，设置手动签收和自动签收没有多大的意义，都默认自动签收，也就是说事务的优先级更高一些。</p>\n<hr />\n<h2>SpringBoot整合ActiveMQ</h2>\n<p><a href=\"https://github.com/lurenha/ActiveMQ\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>\n<hr />\n<p><a href=\"https://note.youdao.com/ynoteshare1/index.html?id=39fb9a2ff543d021d619885806a15f05&amp;type=note\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>', 'JMS :  Java  消息中间件的服务接口规范，activemq 之上是 mq  ， 而 mq 之上是JMS 定义的消息规范 。 activemq 是mq 技术的一种理论实现（与之相类似的实现还有 Kafka  RabbitMQ  RockitMQ  ），而 JMS 是更上一级的规范。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 331, 7, '2020-01-01 15:51:51', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (62, '113. 路径总和 II', '<p>给定一个二叉树和一个目标和，找到所有从根节点到叶子节点路径总和等于给定目标和的路径。</p>\n<p>说明:&nbsp;叶子节点是指没有子节点的节点。</p>\n<p>示例:<br />给定如下二叉树，以及目标和&nbsp;sum = 22，</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<hr />\n<h2>回溯法</h2>\n<p>思路：沿着根节点往下寻找，保存中间的路径并一路求和，当到达叶子节点时：判断当前和是否等于sum？是：将这些路径存入结果集。不是：返回。</p>\n<pre class=\"language-java\"><code> /**\n     * Definition for a binary tree node.\n     * public class TreeNode {\n     * int val;\n     * TreeNode left;\n     * TreeNode right;\n     * TreeNode(int x) { val = x; }\n     * }\n     */\n    class Solution {\n        public List&lt;List&lt;Integer&gt;&gt; pathSum(TreeNode root, int sum) {\n            List&lt;List&lt;Integer&gt;&gt; lists = new ArrayList&lt;&gt;();\n            backstack(root, sum, 0, lists, new ArrayList&lt;&gt;());\n            return lists;\n        }\n\n        void backstack(TreeNode tem, int sum, int res, List&lt;List&lt;Integer&gt;&gt; lists, List&lt;Integer&gt; small) {\n            if (tem == null)\n                return;\n            res += tem.val;//res记录当前路径上的和\n            small.add(tem.val);//回溯入\n            if (tem.left == null &amp;&amp; tem.right == null &amp;&amp; sum == res) {//这条路径是结果之一\n                lists.add(new ArrayList&lt;&gt;(small));\n            } else {\n                backstack(tem.left, sum, res, lists, small);\n                backstack(tem.right, sum, res, lists, small);\n            }\n            small.remove(small.size() - 1);//回溯出\n        }\n    }</code></pre>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /></p>', '给定一个二叉树和一个目标和，找到所有从根节点到叶子节点路径总和等于给定目标和的路径。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 265, 2, '2020-01-06 23:28:26', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (63, 'mysql中InnoDB 主键问题', '<h3>&lt;高性能MYSQL&gt;原话:</h3>\n<p><img src=\"https://images2015.cnblogs.com/blog/268981/201510/268981-20151009212711221-1004867019.jpg\" alt=\"\" /></p>\n<hr />\n<h3>InnoDB引擎表是基于B+树的索引组织表</h3>\n<p>如下这个图就很好的说明了B+的特点</p>\n<p><img src=\"http://img.mp.sohu.com/upload/20170713/d8ae1b14e9bf4b1890146eb803ee9795_th.png\" alt=\"\" /></p>\n<p><strong>B+树的特征：</strong></p>\n<p>1.有k个子树的中间节点包含有k个元素（B树中是k-1个元素），每个元素不保存数据，只用来索引，所有数据都保存在叶子节点。</p>\n<p>2.所有的叶子结点中包含了全部元素的信息，及指向含这些元素记录的指针，且叶子结点本身依关键字的大小自小而大顺序链接。</p>\n<p>3.所有的中间节点元素都同时存在于子节点，在子节点元素中是最大（或最小）元素。</p>\n<p><strong>B+树的优势：</strong></p>\n<p>1.单一节点存储更多的元素，使得查询的IO次数更少。</p>\n<p>2.所有查询都要查找到叶子节点，查询性能稳定。</p>\n<p>3.所有叶子节点形成有序链表，便于范围查询。</p>\n<hr />\n<h3>使用自增主键的好处</h3>\n<p>那么每次插入新的记录，记录就会顺序添加到当前索引节点的后续位置，当一页写满，就会自动开辟一个新的页</p>\n<h3>使用非自增主键坏处</h3>\n<p>由于每次插入主键的值近似于随机，因此每次新纪录都要被插到现有索引页得中间某个位置，此时MySQL不得不为了将新记录插到合适位置而移动数据，甚至目标页面可能已经被回写到磁盘上而从缓存中清掉，此时又要从磁盘上读回来，这增加了很多开销，同时频繁的移动、分页操作造成了大量的碎片，得到了不够紧凑的索引结构，后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。</p>\n<h3>总结</h3>\n<p>如果InnoDB表的数据写入顺序能和B+树索引的叶子节点顺序一致的话，这时候存取效率是最高的。也就是下面这几种情况的存取效率最高：</p>\n<ul class=\"list-paddingleft-2\">\n<li>\n<p>使用自增列(INT/BIGINT类型)做主键，这时候写入顺序是自增的，和B+数叶子节点分裂顺序一致；</p>\n</li>\n<li>\n<p>该表不指定自增列做主键，同时也没有可以被选为主键的唯一索引(上面的条件)，这时候InnoDB会选择内置的ROWID作为主键，写入顺序和ROWID增长顺序一致；</p>\n</li>\n<li>\n<p>如果一个InnoDB表又没有显示主键，又有可以被选择为主键的唯一索引，但该唯一索引可能不是递增关系时(例如字符串、UUID、多字段联合唯一索引的情况)，该表的存取效率就会比较差。</p>\n</li>\n</ul>\n<hr />\n<p><a href=\"https://mp.weixin.qq.com/s/0lpkoaTI8FDAsH6EwwEBfg\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '为什么mysql建议用自增id做主键', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 319, 4, '2020-01-14 13:17:42', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (64, '139. 单词拆分', '<p>给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict，判定&nbsp;s 是否可以被空格拆分为一个或多个在字典中出现的单词。</p>\n<p>说明：</p>\n<p>拆分时可以重复使用字典中的单词。<br />你可以假设字典中没有重复的单词。</p>\n<pre class=\"language-markup\"><code>示例 1：输入: s = \"leetcode\", wordDict = [\"leet\", \"code\"]\n输出: true\n解释: 返回 true 因为 \"leetcode\" 可以被拆分成 \"leet code\"。</code></pre>\n<pre class=\"language-markup\"><code>示例 2：输入: s = \"applepenapple\", wordDict = [\"apple\", \"pen\"]\n输出: true\n解释: 返回 true 因为 \"applepenapple\" 可以被拆分成 \"apple pen apple\"。  注意你可以重复使用字典中的单词。</code></pre>\n<pre class=\"language-markup\"><code>示例 3：输入: s = \"catsandog\", wordDict = [\"cats\", \"dog\", \"sand\", \"and\", \"cat\"]\n输出: false</code></pre>\n<hr />\n<h2>分析：动态规划</h2>\n<p>设dp[n]：代表字符串下标为n时，是否可以被拆分。</p>\n<p>动态转移方程：dp[i]=dp[i-word.length()]&amp;&amp;wordList.has(word)] //word为 i&mdash;（word.length()-i） 构成的单词</p>\n<pre class=\"language-java\"><code>    class Solution {\n        public boolean wordBreak(String s, List&lt;String&gt; wordDict) {\n            boolean[] dp=new boolean[s.length()+1];\n            dp[0]=true;\n            for (int i = 1; i &lt; dp.length; i++) {\n                for (int j = 0; j &lt; i; j++) {\n                    if(dp[j]&amp;&amp;  wordDict.contains(s.substring(j,i))){\n                        dp[i]=true;\n                        break;\n                    }\n                }\n            }\n            return dp[s.length()];\n        }\n    }</code></pre>\n<h3>优化</h3>\n<p>记录单词表里最长单词的长度.从第二重循环时判断如果当前下标i已经大于最长单词了那么j从0肯定无法找到一个单词匹配,则从(i-最长单词长度)开始寻找.</p>\n<pre class=\"language-java\"><code>class Solution {\n   public boolean wordBreak(String s, List&lt;String&gt; wordDict) {\n       boolean dp[]=new boolean[s.length()+1];\n       dp[0]=true;\n       int maxworDict=0;\n       int begin;\n       for(String w:wordDict){\n           maxworDict=Integer.max(maxworDict,w.length());\n       }\n\n        for(int i=1;i&lt;dp.length;i++){\n            begin =i-maxworDict&gt;0 ? i-maxworDict:0;\n            for(int j=begin;j&lt;i;j++){\n                if(dp[j]&amp;&amp;wordDict.contains(s.substring(j,i))){\n                    dp[i]=true;\n                    break;\n                }\n            }\n        }\n        return dp[s.length()];\n    }\n}</code></pre>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /></p>', '给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict，判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 250, 2, '2020-01-17 14:39:44', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (65, '152. 乘积最大子序列', '<p>给定一个整数数组&nbsp;<code>nums</code>&nbsp;，找出一个序列中乘积最大的连续子序列（该序列至少包含一个数）。</p>\n<pre class=\"language-markup\"><code>示例 1:\n\n输入: [2,3,-2,4]\n输出: 6\n解释: 子数组 [2,3] 有最大乘积 6。</code></pre>\n<pre class=\"language-markup\"><code>示例 2:\n\n输入: [-2,0,-1]\n输出: 0\n解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。</code></pre>\n<h3><br />动态规划</h3>\n<ul>\n<li>遍历数组时计算当前最大值，不断更新</li>\n<li>令imax为当前最大值，则当前最大值为 imax = max(imax * nums[i], nums[i])</li>\n<li>由于存在负数，那么会导致最大的变最小的，最小的变最大的。因此还需要维护当前最小值imin，imin = min(imin * nums[i], nums[i])</li>\n<li>当负数出现时则imax与imin进行交换再进行下一步计算</li>\n<li>时间复杂度：O(n)O(n)</li>\n</ul>\n<pre class=\"language-java\"><code>class Solution {\n    public int maxProduct(int[] nums) {\n        int max = Integer.MIN_VALUE, imax = 1, imin = 1;\n        for(int i=0; i&lt;nums.length; i++){\n            if(nums[i] &lt; 0){ \n              int tmp = imax;\n              imax = imin;\n              imin = tmp;\n            }\n            imax = Math.max(imax*nums[i], nums[i]);\n            imin = Math.min(imin*nums[i], nums[i]);\n            \n            max = Math.max(max, imax);\n        }\n        return max;\n    }\n}\n</code></pre>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /><br /><br /></p>\n<hr />\n<p><a href=\"https://leetcode-cn.com/problems/maximum-product-subarray/solution/hua-jie-suan-fa-152-cheng-ji-zui-da-zi-xu-lie-by-g/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '给定一个整数数组 nums ，找出一个序列中乘积最大的连续子序列（该序列至少包含一个数）。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 276, 2, '2020-01-17 16:05:44', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (66, '279. 完全平方数', '<p>给定正整数&nbsp;<em>n</em>，找到若干个完全平方数（比如&nbsp;<code>1, 4, 9, 16, ...</code>）使得它们的和等于<em>&nbsp;n</em>。你需要让组成和的完全平方数的个数最少。</p>\n<pre class=\"language-markup\"><code>示例 1:\n\n输入: n = 12\n输出: 3 \n解释: 12 = 4 + 4 + 4.</code></pre>\n<pre class=\"language-markup\"><code>示例 2:\n\n输入: n = 13\n输出: 2\n解释: 13 = 4 + 9.</code></pre>\n<hr />\n<h3>分析:动态规划</h3>\n<p>我们设方程dp[n]：代表到数字n所需要的最少平方数的个数</p>\n<p>转移方程：dp[i]=Min（dp[1&sup2;]+dp[(i-1&sup2;)]，dp[2&sup2;]+dp[(i-2&sup2;)，dp[3&sup2;]+dp[(i-3&sup2;)，dp[4&sup2;]+dp[(i-4&sup2;)......]） [注：i&gt;=1&sup2;,2&sup2;,3&sup2;,4&sup2;...]</p>\n<pre class=\"language-java\"><code>    class Solution {\n        public int numSquares(int n) {\n            int[] dp=new int[n+1];\n            dp[0]=1;\n            int iMin;\n            for (int i = 1; i &lt;= n; i++) {\n                iMin=Integer.MAX_VALUE;\n                for (int j = 1; j*j &lt;=i ; j++) {\n                    iMin=Math.min(dp[i-j*j]+dp[j*j],iMin);\n                }\n                dp[i]=iMin;\n            }\n            return dp[n];\n        }\n    }</code></pre>\n<p><code>我们发现dp[j*j](dp[1&sup2;],dp[2&sup2;],dp[3&sup2;]...)都是等于1的,所以可以优化为 iMin=Math.min(dp[i-j*j]+1,iMin);//此时:dp[0]=0</code></p>\n<p>说明下为什么第一种 dp[0]=1，第二种dp[0]=0.</p>\n<p>第一种 比如当i=9, j=3时，dp[i-j*j]+dp[j*j]=dp[0]+dp[9],此时dp[9]是数组给的初值0，我们把dp[0]=1作为了最少需要的平方数。</p>\n<p>第二种 比如当i=9, j=3时，dp[i-j*j]+1=dp[0]+1,我们需要让dp[0]=0，满足条件。</p>\n<pre class=\"language-java\"><code>    class Solution {\n        public int numSquares(int n) {\n            int[] dp=new int[n+1];\n            //dp[0]=1;\n            int iMin;\n            for (int i = 1; i &lt;= n; i++) {\n                iMin=Integer.MAX_VALUE;\n                for (int j = 1; j*j &lt;=i ; j++) {\n                    iMin=Math.min(dp[i-j*j]+1,iMin);\n                }\n                dp[i]=iMin;\n            }\n            return dp[n];\n        }\n    }</code></pre>\n<p><code></code></p>\n<p><code></code></p>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /></p>', '给定正整数 n，找到若干个完全平方数（比如 1, 4, 9, 16, ...）使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 256, 2, '2020-01-18 14:40:56', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (67, '322. 零钱兑换', '<p>给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额，返回&nbsp;-1。</p>\n<pre class=\"language-markup\"><code>示例 1:\n\n输入: coins = [1, 2, 5], amount = 11\n输出: 3 \n解释: 11 = 5 + 5 + 1</code></pre>\n<pre class=\"language-markup\"><code>示例 2:\n\n输入: coins = [2], amount = 3\n输出: -1</code></pre>\n<hr />\n<h3>分析：动态规划</h3>\n<p><code>设dp[n]</code>代表到金额<code>i需要的最少硬币数</code></p>\n<p>动态转移方程：<span style=\"font-family: monospace;\"><span style=\"background-color: #bfe6ff;\">dp[i]=Min(dp[i-coins[0]]+1,<span style=\"font-family: monospace;\">dp[i-coins[1]+1,dp[i-coins[2]]+1......</span>)</span></span></p>\n<pre class=\"language-java\"><code>   class Solution {\n        public int coinChange(int[] coins, int amount) {\n            int[] dp=new int[amount+1];\n            dp[0]=0;\n            for (int i = 1; i &lt;= amount; i++) {\n                dp[i]=Integer.MAX_VALUE-1;//防止+1溢出\n            }\n          \n            for (int i = 0; i &lt;= amount; i++) {\n                for (int j = 0; j &lt; coins.length; j++) {\n                    if(i&gt;=coins[j]){\n                         dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);\n                    }\n                }         \n            }\n            if(dp[amount]==Integer.MAX_VALUE-1)\n                 return -1;\n            return dp[amount];\n        }\n    }</code></pre>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p><code></code></p>', '给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额，返回 -1。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 283, 2, '2020-01-18 15:19:36', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (68, '一个方法团灭 6 道股票问题', '<p><a href=\"https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n<h3><code>一、穷举框架首先，还是一样的思路：如何穷举？</code></h3>\n<p>递归其实是符合我们思考的逻辑的，一步步推进，遇到无法解决的就丢给递归，一不小心就做出来了，可读性还很好。缺点就是一旦出错，你也不容易找到错误出现的原因。比如上篇文章的递归解法，肯定还有计算冗余，但确实不容易找到。</p>\n<p>而这里，我们不用递归思想进行穷举，而是利用「状态」进行穷举。我们具体到每一天，看看总共有几种可能的「状态」，再找出每个「状态」对应的「选择」。我们要穷举所有「状态」，穷举的目的是根据对应的「选择」更新状态。听起来抽象，你只要记住「状态」和「选择」两个词就行，下面实操一下就很容易明白了。</p>\n<pre class=\"language-java\"><code>for 状态1 in 状态1的所有取值：\n    for 状态2 in 状态2的所有取值：\n        for ...\n            dp[状态1][状态2][...] = 择优(选择1，选择2...)\n\n</code></pre>\n<p>比如说这个问题，每天都有三种「选择」：买入、卖出、无操作，我们用 buy, sell, rest 表示这三种选择。但问题是，并不是每天都可以任意选择这三种选择的，因为 sell 必须在 buy 之后，buy 必须在 sell 之后。那么 rest 操作还应该分两种状态，一种是 buy 之后的 rest（持有了股票），一种是 sell 之后的 rest（没有持有股票）。而且别忘了，我们还有交易次数 k 的限制，就是说你 buy 还只能在 k &gt; 0 的前提下操作。</p>\n<p>很复杂对吧，不要怕，我们现在的目的只是穷举，你有再多的状态，老夫要做的就是一把梭全部列举出来。这个问题的「状态」有三个，第一个是天数，第二个是允许交易的最大次数，第三个是当前的持有状态（即之前说的 rest 的状态，我们不妨用 1 表示持有，0 表示没有持有）。然后我们用一个三维数组就可以装下这几种状态的全部组合：</p>\n<pre class=\"language-java\"><code>dp[i][k][0 or 1]\n0 &lt;= i &lt;= n-1, 1 &lt;= k &lt;= K\nn 为天数，大 K 为最多交易数\n此问题共 n &times; K &times; 2 种状态，全部穷举就能搞定。\n\nfor 0 &lt;= i &lt; n:\n    for 1 &lt;= k &lt;= K:\n        for s in {0, 1}:\n            dp[i][k][s] = max(buy, sell, rest)\n</code></pre>\n<p>而且我们可以用自然语言描述出每一个状态的含义，比如说 dp[3][2][1] 的含义就是：今天是第三天，我现在手上持有着股票，至今最多进行 2 次交易。再比如 dp[2][3][0] 的含义：今天是第二天，我现在手上没有持有股票，至今最多进行 3 次交易。很容易理解，对吧？</p>\n<p>我们想求的最终答案是 dp[n - 1][K][0]，即最后一天，最多允许 K 次交易，最多获得多少利润。读者可能问为什么不是 dp[n - 1][K][1]？因为 [1] 代表手上还持有股票，[0] 表示手上的股票已经卖出去了，很显然后者得到的利润一定大于前者。</p>\n<p>记住如何解释「状态」，一旦你觉得哪里不好理解，把它翻译成自然语言就容易理解了。</p>\n<h3><code>二、状态转移框架</code></h3>\n<p>现在，我们完成了「状态」的穷举，我们开始思考每种「状态」有哪些「选择」，应该如何更新「状态」。只看「持有状态」，可以画个状态转移图。</p>\n<p><img src=\"https://pic.leetcode-cn.com/c4eb5f0aa4daf7bef4b3b8af95129bb7394ec58e1ba7b191d9104bbd8ff1ccb3-40198bf2f6894018328b250b772b4a17724a983f99ba359b798a289733bffcbc-file_1559885188422-1.png\" alt=\"\" /></p>\n<pre class=\"language-java\"><code>通过这个图可以很清楚地看到，每种状态（0 和 1）是如何转移而来的。根据这个图，我们来写一下状态转移方程：\n\ndp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])\n              max(   选择 rest  ,           选择 sell      )\n\n解释：今天我没有持有股票，有两种可能：\n要么是我昨天就没有持有，然后今天选择 rest，所以我今天还是没有持有；\n要么是我昨天持有股票，但是今天我 sell 了，所以我今天没有持有股票了。\n\ndp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])\n              max(   选择 rest  ,           选择 buy         )\n\n解释：今天我持有着股票，有两种可能：\n要么我昨天就持有着股票，然后今天选择 rest，所以我今天还持有着股票；\n要么我昨天本没有持有，但今天我选择 buy，所以今天我就持有股票了。\n</code></pre>\n<p><code></code></p>\n<p>这个解释应该很清楚了，如果 buy，就要从利润中减去 prices[i]，如果 sell，就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制，我们在选择 buy 的时候，把 k 减小了 1，很好理解吧，当然你也可以在 sell 的时候减 1，一样的。</p>\n<p>现在，我们已经完成了动态规划中最困难的一步：状态转移方程。如果之前的内容你都可以理解，那么你已经可以秒杀所有问题了，只要套这个框架就行了。不过还差最后一点点，就是定义 base case，即最简单的情况。</p>\n<pre class=\"language-markup\"><code>dp[-1][k][0] = 0\n解释：因为 i 是从 0 开始的，所以 i = -1 意味着还没有开始，这时候的利润当然是 0 。\ndp[-1][k][1] = -infinity\n解释：还没开始的时候，是不可能持有股票的，用负无穷表示这种不可能。\ndp[i][0][0] = 0\n解释：因为 k 是从 1 开始的，所以 k = 0 意味着根本不允许交易，这时候利润当然是 0 。\ndp[i][0][1] = -infinity\n解释：不允许交易的情况下，是不可能持有股票的，用负无穷表示这种不可能。\n\n</code></pre>\n<pre class=\"language-java\"><code>把上面的状态转移方程总结一下：\n\nbase case：\ndp[-1][k][0] = dp[i][0][0] = 0\ndp[-1][k][1] = dp[i][0][1] = -infinity\n\n状态转移方程：\ndp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])\ndp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])</code></pre>\n<p>读者可能会问，这个数组索引是 -1 怎么编程表示出来呢，负无穷怎么表示呢？这都是细节问题，有很多方法实现。现在完整的框架已经完成，下面开始具体化。</p>\n<h3>三、秒杀题目</h3>\n<p><strong>第一题，k = 1</strong></p>\n<p>直接套状态转移方程，根据 base case，可以做一些化简：</p>\n<pre class=\"language-java\"><code>dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])\ndp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) \n            = max(dp[i-1][1][1], -prices[i])\n解释：k = 0 的 base case，所以 dp[i-1][0][0] = 0。\n\n现在发现 k 都是 1，不会改变，即 k 对状态转移已经没有影响了。\n可以进行进一步化简去掉所有 k：\ndp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])\ndp[i][1] = max(dp[i-1][1], -prices[i])\n直接写出代码：\n\nint n = prices.length;\nint[][] dp = new int[n][2];\nfor (int i = 0; i &lt; n; i++) {\n    dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);\n    dp[i][1] = Math.max(dp[i-1][1], -prices[i]);\n}\nreturn dp[n - 1][0];\n\n\n</code></pre>\n<pre class=\"language-java\"><code>显然 i = 0 时 dp[i-1] 是不合法的。这是因为我们没有对 i 的 base case 进行处理。可以这样处理：\n\nfor (int i = 0; i &lt; n; i++) {\n    if (i - 1 == -1) {\n        dp[i][0] = 0;\n        // 解释：\n        //   dp[i][0] \n        // = max(dp[-1][0], dp[-1][1] + prices[i])\n        // = max(0, -infinity + prices[i]) = 0\n        dp[i][1] = -prices[i];\n        //解释：\n        //   dp[i][1] \n        // = max(dp[-1][1], dp[-1][0] - prices[i])\n        // = max(-infinity, 0 - prices[i]) \n        // = -prices[i]\n        continue;\n    }\n    dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);\n    dp[i][1] = Math.max(dp[i-1][1], -prices[i]);\n}\nreturn dp[n - 1][0];</code></pre>\n<p><strong>第二题，k = +infinity</strong></p>\n<pre class=\"language-java\"><code>如果 k 为正无穷，那么就可以认为 k 和 k - 1 是一样的。可以这样改写框架：\n\ndp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])\ndp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])\n            = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])\n\n我们发现数组中的 k 已经不会改变了，也就是说不需要记录 k 这个状态了：\ndp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])\ndp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])</code></pre>\n<pre class=\"language-java\"><code>直接翻译成代码：\n\nint maxProfit_k_inf(int[] prices) {\n    int n = prices.length;\n    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;\n    for (int i = 0; i &lt; n; i++) {\n        int temp = dp_i_0;\n        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);\n        dp_i_1 = Math.max(dp_i_1, temp - prices[i]);\n    }\n    return dp_i_0;\n}</code></pre>\n<p><strong>第三题，k = +infinity with cooldown</strong></p>\n<p>&nbsp;</p>\n<pre class=\"language-java\"><code>每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可：\n\ndp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])\ndp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])\n解释：第 i 天选择 buy 的时候，要从 i-2 的状态转移，而不是 i-1 。\n</code></pre>\n<pre class=\"language-java\"><code>翻译成代码：\n\nint maxProfit_with_cool(int[] prices) {\n    int n = prices.length;\n    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;\n    int dp_pre_0 = 0; // 代表 dp[i-2][0]\n    for (int i = 0; i &lt; n; i++) {\n        int temp = dp_i_0;\n        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);\n        dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);\n        dp_pre_0 = temp;\n    }\n    return dp_i_0;\n}</code></pre>\n<p><strong>第四题，k = +infinity with fee</strong></p>\n<pre class=\"language-java\"><code>每次交易要支付手续费，只要把手续费从利润中减去即可。改写方程：\n\ndp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])\ndp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)\n解释：相当于买入股票的价格升高了。\n在第一个式子里减也是一样的，相当于卖出股票的价格减小了。\n</code></pre>\n<pre class=\"language-java\"><code>直接翻译成代码：\n\nint maxProfit_with_fee(int[] prices, int fee) {\n    int n = prices.length;\n    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;\n    for (int i = 0; i &lt; n; i++) {\n        int temp = dp_i_0;\n        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);\n        dp_i_1 = Math.max(dp_i_1, temp - prices[i] - fee);\n    }\n    return dp_i_0;\n}</code></pre>\n<p><strong>第五题，k = 2</strong></p>\n<p>&nbsp;</p>\n<pre class=\"language-java\"><code>k = 2 和前面题目的情况稍微不同，因为上面的情况都和 k 的关系不太大。要么 k 是正无穷，状态转移和 k 没关系了；要么 k = 1，跟 k = 0 这个 base case 挨得近，最后也没有存在感。\n\n这道题 k = 2 和后面要讲的 k 是任意正整数的情况中，对 k 的处理就凸显出来了。我们直接写代码，边写边分析原因。\n\n原始的动态转移方程，没有可化简的地方\ndp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])\ndp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])</code></pre>\n<pre class=\"language-java\"><code>int max_k = 2;\nint[][][] dp = new int[n][max_k + 1][2];\nfor (int i = 0; i &lt; n; i++) {\n    for (int k = max_k; k &gt;= 1; k--) {\n        if (i - 1 == -1) { \n            /* 处理 base case */\n            dp[i][k][0] = 0;\n            dp[i][k][1] = -prices[i];\n            continue;\n        }\n        dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);\n        dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);\n    }\n}\n// 穷举了 n &times; max_k &times; 2 个状态，正确。\nreturn dp[n - 1][max_k][0];</code></pre>\n<p><strong>第六题，k = any integer</strong></p>\n<p>有了上一题 k = 2 的铺垫，这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误，原来是传入的 k 值会非常大，dp 数组太大了。现在想想，交易次数 k 最多有多大呢？</p>\n<p>一次交易由买入和卖出构成，至少需要两天。所以说有效的限制 k 应该不超过 n/2，如果超过，就没有约束作用了，相当于 k = +infinity。这种情况是之前解决过的。</p>\n<p>直接把之前的代码重用：</p>\n<pre class=\"language-java\"><code>int maxProfit_k_any(int max_k, int[] prices) {\n    int n = prices.length;\n    if (max_k &gt; n / 2) \n        return maxProfit_k_inf(prices);\n\n    int[][][] dp = new int[n][max_k + 1][2];\n    for (int i = 0; i &lt; n; i++) \n        for (int k = max_k; k &gt;= 1; k--) {\n            if (i - 1 == -1) { \n                /* 处理 base case */\n                dp[i][k][0] = 0;\n                dp[i][k][1] = -prices[i];\n                continue;\n            }\n            dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);\n            dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);     \n        }\n    return dp[n - 1][max_k][0];\n}</code></pre>\n<p>&nbsp;</p>', '用一个状态转移方程秒杀了 6 道股票买卖问题', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 299, 2, '2020-01-19 17:27:17', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (69, '47. 全排列 II', '<h2>47. 全排列 II</h2>\n<p>给定一个可<span style=\"color: #ff0000;\">包含重复数字</span>的序列，返回所有<span style=\"color: #ff0000;\">不重复</span>的全排列。</p>\n<p>示例:</p>\n<p>输入: [1,1,2]<br />输出:<br />[<br />[1,1,2],<br />[1,2,1],<br />[2,1,1]<br />]</p>\n<hr />\n<p>分析：在全排列的基础上加上了限制条件不重复。可以直接用全排列的代码进行剪纸操作。如图</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p>我们发现在每层计算中，只需要计算第一次出现的数字，再出现重复的数字无需运算。</p>\n<p>所以我们可以在每层中使用Set进行去重。</p>\n<hr />\n<pre class=\"language-java\"><code>class Solution {\n    public List&lt;List&lt;Integer&gt;&gt; permuteUnique(int[] nums) {\n        List&lt;List&lt;Integer&gt;&gt; res=new ArrayList&lt;&gt;();\n        backstack(nums,0,nums.length,res);\n        return res;\n    }\n\n    private void backstack(int[] nums,int begin,int end, List&lt;List&lt;Integer&gt;&gt; res){\n        if(begin==end){\n            List&lt;Integer&gt; tem = new ArrayList&lt;Integer&gt;();\n            for (int n = 0; n &lt; nums.length; n++) {\n                tem.add(nums[n]);\n            }\n            res.add(tem);\n            return;\n        }\n        \n        Set&lt;Integer&gt; set=new HashSet&lt;&gt;();\n        for (int i = begin; i &lt;end; i++) {//begin-end 之间的全排列\n           if(set.contains(nums[i]))//去重\n                continue;\n            set.add(nums[i]);\n            swap(nums,i,begin);\n            backstack(nums,begin+1,end,res);\n            swap(nums,i,begin);\n\n        }\n    }\n\n    private  void swap(int[] nums, int x, int y) {\n        int a = nums[x];\n        nums[x] = nums[y];\n        nums[y] = a;\n    }\n}</code></pre>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /></p>', '47. 全排列 II', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 355, 2, '2020-01-31 15:29:01', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (70, 'springboot属性注入', '<h3>方式一</h3>\n<p>通过@Value为属性注入值。</p>\n<p>1.使用application.properties文件，添加配置信息。</p>\n<pre class=\"language-markup\"><code>jdbc.driverClassName=com.mysql.jdbc.Driver\n\njdbc.url=jdbc:mysql://127.0.0.1:3306/leyou\n\njdbc.username=root\n\njdbc.password=123</code></pre>\n<p>2.创建JdbcConfiguration类，使用spring中的value注解对每个属性进行注入,用bean注解将返回值添加到容器中</p>\n<pre class=\"language-markup\"><code>@Configuration\n\n@PropertySource(\"classpath:jdbc.properties\")\n\npublic class JdbcConfiguration {\n\n \n\n    @Value(\"${jdbc.url}\")\n\n    String url;\n\n    @Value(\"${jdbc.driverClassName}\")\n\n    String driverClassName;\n\n    @Value(\"${jdbc.username}\")\n\n    String username;\n\n    @Value(\"${jdbc.password}\")\n\n    String password;\n\n \n\n    @Bean\n\n    public DataSource dataSource() {\n\n        DruidDataSource dataSource = new DruidDataSource();\n\n        dataSource.setUrl(url);\n\n        dataSource.setDriverClassName(driverClassName);\n\n        dataSource.setUsername(username);\n\n        dataSource.setPassword(password);\n\n        return dataSource;\n\n    }\n\n}\n</code></pre>\n<ul>\n<li>@Configuration：声明JdbcConfiguration是一个配置类。</li>\n<li>@PropertySource：指定属性文件的路径是:classpath:jdbc.properties</li>\n<li>通过@Value为属性注入值。</li>\n<li>通过@Bean将 dataSource()方法声明为一个注册Bean的方法，Spring会自动调用该方法，将方法的返回值加入Spring容器中。相当于以前的bean标签</li>\n<li>然后就可以在任意位置通过@Autowired注入DataSource了！</li>\n</ul>\n<hr />\n<h3>方式二（常用）</h3>\n<p>使用@ConfigurationProperties注入</p>\n<p>需求：我们可能在系统中需要用到多种文件存储模式（服务器本地存储，七牛云对象存储，阿里云等）</p>\n<p>1.配置文件application.yml是这样的（服务器本地存储，七牛云对象存储）</p>\n<pre class=\"language-markup\"><code>myapplication:\n  # 对象存储配置\n  storage:\n    # 当前工作的对象存储模式，分别是local,qiniu\n    active: local\n\n    # 本地对象存储配置信息\n    local:\n      storagePath: storage\n      address: http://localhost:8080/xxx/\n\n    # 七牛云对象存储配置信息\n    qiniu:\n      endpoint: http://s3-cn-north-1.qiniucs.com\n      accessKey: xxxxxxxxxxxxxxx\n      secretKey: xxxxxxxxxxxxxxx\n      bucketName: pengcloud01\n\n </code></pre>\n<p>&nbsp;</p>\n<p>2.使用@ConfigurationProperties注解将配置属性注入</p>\n<pre class=\"language-java\"><code>/***\n * 将配置文件（myapplication.storage）转换成对象\n */\n@ConfigurationProperties(prefix = \"myapplication.storage\")\npublic class StorageProperties {\n    private String active;\n    private Local local;\n    private Qiniu qiniu;\n\n    public String getActive() {\n        return active;\n    }\n\n    public void setActive(String active) {\n        this.active = active;\n    }\n\n    public Local getLocal() {\n        return local;\n    }\n\n    public void setLocal(Local local) {\n        this.local = local;\n    }\n\n    public Qiniu getQiniu() {\n        return qiniu;\n    }\n\n    public void setQiniu(Qiniu qiniu) {\n        this.qiniu = qiniu;\n    }\n\n\n    public static class Local {\n        private String address;\n        private String storagePath;\n\n        public String getAddress() {\n            return address;\n        }\n\n        public void setAddress(String address) {\n            this.address = address;\n        }\n\n        public String getStoragePath() {\n            return storagePath;\n        }\n\n        public void setStoragePath(String storagePath) {\n            this.storagePath = storagePath;\n        }\n    }\n\n    public static class Qiniu {\n        private String endpoint;\n        private String accessKey;\n        private String secretKey;\n        private String bucketName;\n\n        public String getEndpoint() {\n            return endpoint;\n        }\n\n        public void setEndpoint(String endpoint) {\n            this.endpoint = endpoint;\n        }\n\n        public String getAccessKey() {\n            return accessKey;\n        }\n\n        public void setAccessKey(String accessKey) {\n            this.accessKey = accessKey;\n        }\n\n        public String getSecretKey() {\n            return secretKey;\n        }\n\n        public void setSecretKey(String secretKey) {\n            this.secretKey = secretKey;\n        }\n\n        public String getBucketName() {\n            return bucketName;\n        }\n\n        public void setBucketName(String bucketName) {\n            this.bucketName = bucketName;\n        }\n    }\n\n\n\n   \n\n}\n</code></pre>\n<p>3.编写配置类根据具体active选择对应的实现，最终我们就可以注入@Autowired StorageService 进行使用了</p>\n<pre class=\"language-java\"><code>/***\n * 根据配置文件active注入对应的实现类\n */\n@Configuration\n@EnableConfigurationProperties(StorageProperties.class)//要用到的属性类\npublic class StorageAutoConfiguration {\n\n    //可以使用注解注入\n    // @Autowired\n    //private StorageProperties properties;\n\n    //也可以使用构造函数传入\n    private final StorageProperties properties;\n    public StorageAutoConfiguration(StorageProperties properties) {\n        this.properties = properties;\n    }\n\n    @Bean\n    public StorageService storageService() {\n        StorageService storageService = new StorageService();\n        String active = this.properties.getActive();\n        storageService.setActive(active);\n        if (active.equals(\"local\")) {\n            storageService.setStorage(localStorage());\n        } else if (active.equals(\"qiniu\")) {\n            storageService.setStorage(qiniuStorage());\n        } else {\n            throw new RuntimeException(\"当前存储模式 \" + active + \" 不支持\");\n        }\n        return storageService;\n    }\n\n    @Bean\n    public LocalStorage localStorage() {\n        LocalStorage localStorage = new LocalStorage();\n        StorageProperties.Local local = this.properties.getLocal();\n        localStorage.setAddress(local.getAddress());\n        localStorage.setStoragePath(local.getStoragePath());\n        return localStorage;\n    }\n\n    @Bean\n    public QiniuStorage qiniuStorage() {\n        QiniuStorage qiniuStorage = new QiniuStorage();\n        StorageProperties.Qiniu qiniu = this.properties.getQiniu();\n        qiniuStorage.setAccessKey(qiniu.getAccessKey());\n        qiniuStorage.setSecretKey(qiniu.getSecretKey());\n        qiniuStorage.setBucketName(qiniu.getBucketName());\n        qiniuStorage.setEndpoint(qiniu.getEndpoint());\n        return qiniuStorage;\n    }\n\n\n\n}</code></pre>\n<p>注：StorageService是提供存储服务的代理类，具体的存储逻辑由Storage接口的具体实现类去做。（如LocalStorage，QiniuStorage）</p>\n<p>具体逻辑实现可以参照这个<a href=\"https://github.com/lurenha/qiniu-demo\" target=\"_blank\" rel=\"noopener\">qiniu-demo</a></p>\n<pre class=\"language-java\"><code>\n/**\n * 提供存储服务类，所有存储服务均由该类对外提供\n */\npublic class StorageService  {\n    private String active;\n    private Storage storage;\n\n    public String getActive() {\n        return active;\n    }\n\n    public void setActive(String active) {\n        this.active = active;\n    }\n\n    public Storage getStorage() {\n        return storage;\n    }\n\n    public void setStorage(Storage storage) {\n        this.storage = storage;\n    }\n\n    /**\n     * 存储一个文件对象\n     *\n     * @param inputStream   文件输入流\n     * @param contentLength 文件长度\n     * @param contentType   文件类型\n     * @param fileName      文件索引名\n     */\n    public boolean store(InputStream inputStream, long contentLength, String contentType, String fileName) {\n        String key=generateKey(fileName);\n        storage.store(inputStream, contentLength, contentType, key);\n//        String url = generateUrl(key);\n//        业务相关\n//        保存key等信息到数据库\n        return true;\n    }\n    private String generateKey(String originalFilename) {\n        int index = originalFilename.lastIndexOf(\'.\');\n        String suffix = originalFilename.substring(index);\n        String key = UUID.randomUUID().toString();\n        key = key.replace(\"-\", \"\");\n        key+=suffix;\n        return key;\n    }\n\n    public Stream&lt;Path&gt; loadAll() {\n        return storage.loadAll();\n    }\n\n    public Path load(String keyName) {\n        return storage.load(keyName);\n    }\n\n    public Resource loadAsResource(String keyName) {\n        return storage.loadAsResource(keyName);\n    }\n\n    public void delete(String keyName) {\n        storage.delete(keyName);\n    }\n\n    private String generateUrl(String keyName) {\n        return storage.generateUrl(keyName);\n    }\n}\n</code></pre>', 'SpringBoot的两种属性值注入的方式', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 313, 6, '2020-02-04 18:40:56', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (71, 'SpringBoot常用依赖包', '<pre class=\"language-markup\"><code> &lt;dependencies&gt;\n        &lt;!-- springboot web --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!--aop--&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-aop&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!--热部署配置--&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- lombok --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;\n            &lt;artifactId&gt;lombok&lt;/artifactId&gt;\n            &lt;version&gt;1.18.10&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- mybatis --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt;\n            &lt;version&gt;${mybatis.version}&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- 分页插件 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;com.github.pagehelper&lt;/groupId&gt;\n            &lt;artifactId&gt;pagehelper-spring-boot-starter&lt;/artifactId&gt;\n            &lt;version&gt;${pagehelper.version}&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- springboot test --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- 连接池 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;com.alibaba&lt;/groupId&gt;\n            &lt;artifactId&gt;druid&lt;/artifactId&gt;\n            &lt;version&gt;${druid.version}&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- mysql连接包 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;mysql&lt;/groupId&gt;\n            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;\n            &lt;scope&gt;runtime&lt;/scope&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- json --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;net.sf.json-lib&lt;/groupId&gt;\n            &lt;artifactId&gt;json-lib&lt;/artifactId&gt;\n            &lt;version&gt;2.4&lt;/version&gt;\n            &lt;classifier&gt;jdk15&lt;/classifier&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- 工具包 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;\n            &lt;artifactId&gt;commons-lang3&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- JWT token --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;com.auth0&lt;/groupId&gt;\n            &lt;artifactId&gt;java-jwt&lt;/artifactId&gt;\n            &lt;version&gt;${jwt.version}&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- shiro --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.apache.shiro&lt;/groupId&gt;\n            &lt;artifactId&gt;shiro-spring&lt;/artifactId&gt;\n            &lt;version&gt;${shiro.vaesion}&lt;/version&gt;\n        &lt;/dependency&gt;\n\n    &lt;/dependencies&gt;</code></pre>', 'Maven常用依赖包', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 0, 1, 286, 6, '2019-12-01 15:58:46', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (72, 'Bloom Filter(布隆过滤器)', '<h2 id=\"什么是布隆过滤器\">哈希映射存在的问题</h2>\n<p>现在有大量的数据，而这些数据的大小已经远远超出了服务器的内存，现在再给你一个数据，如何判断给你的数据在不在其中。如果服务器的内存足够大，那么用HashMap是一个不错的解决方案，理论上的时间复杂度可以达到O(1)，但是 HashMap 的实现也有缺点，例如存储容量占比高，考虑到负载因子的存在，通常空间是不能被用满的，而一旦你的值很多例如上亿的时候，那 HashMap 占据的内存大小就变得很可观了。这个时候就可以使用&ldquo;布隆过滤器&rdquo;来解决这个问题。但是还是同样的，会有一定的&ldquo;误判率&rdquo;。</p>\n<h2 id=\"什么是布隆过滤器\">什么是布隆过滤器</h2>\n<p>布隆过滤器是一个叫&ldquo;布隆&rdquo;的人提出的，它本身是一个很长的二进制向量，既然是二进制的向量，那么显而易见的，存放的不是0，就是1。</p>\n<p>所以布隆过滤器大概是是一个 bit 向量或者说 bit 数组。</p>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/2785001-07e149c32a2608fa.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" alt=\"https://upload-images.jianshu.io/upload_images/2785001-07e149c32a2608fa.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" width=\"600\" height=\"151\" /></p>\n<div>\n<div>如果我们要映射一个值到布隆过滤器中，我们需要使用多个不同的哈希函数生成多个哈希值，并对每个生成的哈希值指向的 bit 位置 1，例如针对值 &ldquo;baidu&rdquo; 和三个不同的哈希函数分别生成了哈希值 1、4、7，则上图转变为：</div>\n<img src=\"https://upload-images.jianshu.io/upload_images/2785001-12449becdb038afd.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" alt=\"https://upload-images.jianshu.io/upload_images/2785001-12449becdb038afd.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" width=\"600\" height=\"290\" /><br /><br /></div>\n<div>Ok，我们现在再存一个值 &ldquo;tencent&rdquo;，如果哈希函数返回 3、4、8 的话，图继续变为：</div>\n<div><img src=\"https://upload-images.jianshu.io/upload_images/2785001-802577f6332d76b4.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" alt=\"https://upload-images.jianshu.io/upload_images/2785001-802577f6332d76b4.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" width=\"600\" height=\"310\" /></div>\n<div>\n<p>值得注意的是，4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位，因此它被覆盖了。现在我们如果想查询 &ldquo;dianping&rdquo; 这个值是否存在，哈希函数返回了 1、5、8三个值，结果我们发现 5 这个 bit 位上的值为 0，说明没有任何一个值映射到这个 bit 位上，因此我们可以很确定地说 &ldquo;dianping&rdquo; 这个值不存在。而当我们需要查询 &ldquo;baidu&rdquo; 这个值是否存在的话，那么哈希函数必然会返回 1、4、7，然后我们检查发现这三个 bit 位上的值均为 1，那么我们可以说 &ldquo;baidu&rdquo; 存在了么？答案是不可以，只能是 &ldquo;baidu&rdquo; 这个值可能存在。</p>\n<p>这是为什么呢？答案跟简单，因为随着增加的值越来越多，被置为 1 的 bit 位也会越来越多，这样某个值 &ldquo;taobao&rdquo; 即使没有被存储过，但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ，那么程序还是会判断 &ldquo;taobao&rdquo; 这个值存在。</p>\n<h2>支持删除么</h2>\n<div>\n<div>\n<p>目前我们知道布隆过滤器可以支持 add 和 isExist 操作，那么 delete 操作可以么，答案是不可以，例如上图中的 bit 位 4 被两个值共同覆盖的话，一旦你删除其中一个值例如 &ldquo;tencent&rdquo; 而将其置位 0，那么下次判断另一个值例如 &ldquo;baidu&rdquo; 是否存在的话，会直接返回 false，而实际上你并没有删除它。</p>\n<p>如何解决这个问题，答案是计数删除。但是计数删除需要存储一个数值，而不是原先的 bit 位，会增大占用的内存大小。这样的话，增加一个值就是将对应索引槽上存储的值加一，删除则是减一，判断是否存在则是看值是否大于0。</p>\n</div>\n<h2>如何选择哈希函数个数和布隆过滤器长度</h2>\n<div>\n<div>\n<p>很显然，过小的布隆过滤器很快所有的 bit 位均为 1，那么查询任何值都会返回&ldquo;可能存在&rdquo;，起不到过滤的目的了。布隆过滤器的长度会直接影响误报率，布隆过滤器越长其误报率越小。</p>\n<p>另外，哈希函数的个数也需要权衡，个数越多则布隆过滤器 bit 位置位 1 的速度越快，且布隆过滤器的效率越低；但是如果太少的话，那我们的误报率会变高。</p>\n</div>\n<img src=\"https://upload-images.jianshu.io/upload_images/2785001-76dccfbdc9d7bdb1.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" alt=\"https://upload-images.jianshu.io/upload_images/2785001-76dccfbdc9d7bdb1.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" width=\"600\" height=\"342\" /></div>\n<div>k 为哈希函数个数，m 为布隆过滤器长度，n 为插入的元素个数，p 为误报率。<br />至于如何推导这个公式，我在知乎发布的<a href=\"https://zhuanlan.zhihu.com/p/43263751\" target=\"_blank\" rel=\"nofollow noopener\">文章</a>有涉及，感兴趣可以看看，不感兴趣的话记住上面这个公式就行了。</div>\n<div>\n<h2>使用场景</h2>\n<ol>\n<li>判断给定数据是否存在：比如判断一个数字是否在于包含大量数字的数字集中（数字集很大，5亿以上！）、 防止缓存穿透（判断请求的数据是否有效避免直接绕过缓存请求数据库）等等、邮箱的垃圾邮件过滤、黑名单功能等等。</li>\n<li>去重：比如爬给定网址的时候对已经爬取过的 URL 去重。</li>\n</ol>\n<h3 id=\"guava实现布隆过滤器\">guava实现布隆过滤器</h3>\n<p>现在相信你对布隆过滤器应该有一个比较感性的认识了，布隆过滤器核心思想其实并不难，难的在于如何设计随机映射函数，到底映射几次，二进制向量的长度设置为多少比较好，这可能就不是一般的开发可以驾驭的了，好在Google大佬给我们提供了开箱即用的组件，来帮助我们实现布隆过滤器，现在就让我们看看怎么Google大佬送给我们的&ldquo;礼物&rdquo;吧。</p>\n<pre class=\"language-markup\"><code> &lt;dependency&gt;\n      &lt;groupId&gt;com.google.guava&lt;/groupId&gt;\n      &lt;artifactId&gt;guava&lt;/artifactId&gt;\n      &lt;version&gt;19.0&lt;/version&gt;\n &lt;/dependency&gt;</code></pre>\n<pre class=\"language-java\"><code> private static int size = 1000000;//预计要插入多少数据\n\n    private static double fpp = 0.01;//期望的误判率\n\n    private static BloomFilter&lt;Integer&gt; bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);\n\n    public static void main(String[] args) {\n        //插入数据\n        for (int i = 0; i &lt; 1000000; i++) {\n            bloomFilter.put(i);\n        }\n        int count = 0;\n        for (int i = 1000000; i &lt; 2000000; i++) {\n            if (bloomFilter.mightContain(i)) {\n                count++;\n                System.out.println(i + \"误判了\");\n            }\n        }\n        System.out.println(\"总共的误判数:\" + count);\n    }</code></pre>\n<p>代码简单分析：<br />我们定义了一个布隆过滤器，有两个重要的参数，分别是 我们预计要插入多少数据，我们所期望的误判率，误判率不能为0。<br />我向布隆过滤器插入了0-1000000，然后用1000000-2000000来测试误判率。</p>\n<p>运行结果：</p>\n<pre class=\"language-java\"><code>1999501误判了\n1999567误判了\n1999640误判了\n1999697误判了\n1999827误判了\n1999942误判了\n总共的误判数:10314</code></pre>\n<p>现在总共有100万数据是不存在的，误判了10314次，我们计算下误判率，和我们定义的期望误判率0.01相差无几。</p>\n</div>\n<div><hr /><a href=\"https://www.jianshu.com/p/2104d11ee0a2\" target=\"_blank\" rel=\"noopener\">参考资料1</a></div>\n</div>\n</div>\n<p><a href=\"https://www.cnblogs.com/xujian2014/p/5491286.html\" target=\"_blank\" rel=\"noopener\">参考资料2</a></p>', '布隆过滤器', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 277, 6, '2020-02-15 18:23:25', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (73, 'BitSet', '<h2>什么是BitSet？</h2>\n<p>　BitSet类实现了一个按需增长的位向量。位Set的每一个组件都有一个boolean值。用非负的整数将BitSet的位编入索引。可以对每个编入索引的位进行测试、设置或者清除。通过逻辑与、逻辑或和逻辑异或操作，可以使用一个 BitSet修改另一个&nbsp;BitSet的内容。&nbsp;</p>\n<p>　　默认情况下，set 中所有位的初始值都是false。&nbsp;</p>\n<p>　　每个位 set 都有一个当前大小，也就是该位 set 当前所用空间的位数。注意，这个大小与位 set 的实现有关，所以它可能随实现的不同而更改。位 set 的长度与位 set 的逻辑长度有关，并且是与实现无关而定义的。&nbsp;</p>\n<h2><span class=\"number\"><span class=\"keyword\">使用场景</span></span></h2>\n<p>常见的应用场景是对海量数据进行一些统计工作，比如日志分析、用户数统计等。</p>\n<p>　　阿里的面试问过一道题：有1千万个随机数，随机数的范围在1到1亿之间。现在要求写出一种算法，将1到1亿之间没有在随机数中的数求出来？</p>\n<p>　　代码示例如下：　</p>\n<pre class=\"language-java\"><code>public class Alibaba\n{\n    public static void main(String[] args)\n    {\n        Random random=new Random();\n        \n        List&lt;Integer&gt; list=new ArrayList&lt;&gt;();\n        for(int i=0;i&lt;10000000;i++)\n        {\n            int randomResult=random.nextInt(100000000);\n            list.add(randomResult);\n        }\n        System.out.println(\"产生的随机数有\");\n        for(int i=0;i&lt;list.size();i++)\n        {\n            System.out.println(list.get(i));\n        }\n        BitSet bitSet=new BitSet(100000000);\n        for(int i=0;i&lt;10000000;i++)\n        {\n            bitSet.set(list.get(i));\n        }\n        \n        System.out.println(\"0~1亿不在上述随机数中有\"+bitSet.size());\n        for (int i = 0; i &lt; 100000000; i++)\n        {\n            if(!bitSet.get(i))\n            {\n                System.out.println(i);\n            }\n        }     \n    }\n}</code></pre>\n<h2>原理</h2>\n<p>Java的BitSet使用一个Long（一共64位）的数组中的每一位（bit）是否为<code>1</code>来表示当前Index的数是否存在。但是BitSet又是如何实现的呢？其实只需要理解其中的两个方法：</p>\n<ul>\n<li>set</li>\n<li>get</li>\n</ul>\n<h2>set</h2>\n<p>先看源代码：</p>\n<pre class=\"language-java\"><code>public void set(int bitIndex) {\n    if (bitIndex &lt; 0)\n        throw new IndexOutOfBoundsException(\"bitIndex &lt; 0: \" + bitIndex);\n\n    int wordIndex = wordIndex(bitIndex);\n    expandTo(wordIndex);\n\n    words[wordIndex] |= (1L &lt;&lt; bitIndex); // Restores invariants\n\n    checkInvariants();\n}</code></pre>\n<p>除了判断给的值是否小于0的那两句，我们依次来看一下这个函数的每一句代码。</p>\n<h3>wordIndex</h3>\n<p>第一句就是计算wordIndex，通过<code>wordIndex</code>函数获取值。代码如下：</p>\n<pre class=\"language-java\"><code>private static int wordIndex(int bitIndex) {\n    return bitIndex &gt;&gt; 6;\n}</code></pre>\n<p><strong>为什么是6呢？而不是其他值呢？</strong></p>\n<div>\n<div>因为在Java里Long类型是64位，要计算给定的参数<code>bitIndex</code>应该放在数组（在BitSet里存在<code>word</code>的实例变量里）的哪个long里，只需要计算：<code>bitIndex / 64</code>即可，这里使用<code>&gt;&gt;</code>来代替除法（因为位运算要比除法效率高）。而64正好是2的6次幂。</div>\n<div>通过<code>wordIndex</code>函数就能计算出参数<code>bitIndex</code>应该存放在<code>words</code>数组里的哪一个long里。</div>\n<div>\n<h3>expandTo</h3>\n<pre class=\"language-java\"><code>private void expandTo(int wordIndex) {\n    int wordsRequired = wordIndex+1;\n    if (wordsInUse &lt; wordsRequired) {\n        ensureCapacity(wordsRequired);\n        wordsInUse = wordsRequired;\n    }\n}</code></pre>\n<div>\n<div>从上面已经知道在BitSet里是通过一个Long数组（<code>words</code>）来存放数据的，这里的<code>expandTo</code>方法就是用来判断<code>words</code>数组的长度是否大于当前所计算出来的<code>wordIndex</code>，如果超过当前<code>words</code>数组的长度，也即是存不下，则新加一个long数到<code>words</code>里(ensureCapacity(wordsRequired)所实现的)。</div>\n<div>\n<h3>Restores invariants</h3>\n<pre class=\"language-java\"><code>words[wordIndex] |= (1L &lt;&lt; bitIndex); // Restores invariants</code></pre>\n</div>\n</div>\n</div>\n<pre class=\"language-java\"><code>System.out.println(Integer.toBinaryString(1&lt;&lt;0));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;1));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;2));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;3));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;4));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;5));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;6));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;7));​</code></pre>\n<p>输出是：</p>\n<pre class=\"language-java\"><code>1\n10\n100\n1000\n10000\n100000\n1000000\n10000000</code></pre>\n<div>\n<div>上面所有的输出中 <code>1</code> 所在的位置，正好是第1，2，3，4，5，6，7，8（Java数组的Index从0开始）位。BitSet正是通过这种方式，将所给的<code>bitIndex</code>对应位设置成1，表示这个数已经存在了。（因为BitSet是使用的Long，所以要使用1L来进行位移）（当bitIndex大于63时会轮回 如1&lt;&lt;0等于1&lt;&lt;64等于1&lt;&lt;128）。</div>\n<div>搞懂了<code>(1L &lt;&lt; bitIndex)</code>，剩下的就是用<code>|</code>来将当前算出来的和以前的值进行合并了<code>words[wordIndex] |= (1L &lt;&lt; bitIndex);</code>。</div>\n<div>\n<h2>get</h2>\n<p>搞懂了<code>set</code>方法，那么<code>get</code>方法也就好懂了，整体意思就是算出来所给定的<code>bitIndex</code>所对应的位数是否为<code>1</code>即可。先看看代码：</p>\n<pre class=\"language-java\"><code>public boolean get(int bitIndex) {\n    if (bitIndex &lt; 0)\n        throw new IndexOutOfBoundsException(\"bitIndex &lt; 0: \" + bitIndex);\n\n    checkInvariants();\n\n    int wordIndex = wordIndex(bitIndex);\n    return (wordIndex &lt; wordsInUse)\n        &amp;&amp; ((words[wordIndex] &amp; (1L &lt;&lt; bitIndex)) != 0);\n}</code></pre>\n<div>\n<div>计算<code>wordIndex</code>在上面set方法里已经说明了，就不再细述。这个方法里，最重要的就只有：<code>words[wordIndex] &amp; (1L &lt;&lt; bitIndex)</code>。这里<code>(1L &lt;&lt; bitIndex)</code>也已经做过说明，就是算出一个数，只有<code>bitIndex</code>位上为1，其他都为0，然后再和<code>words[wordIndex]</code>做<code>&amp;</code>计算，如果<code>words[wordIndex]</code>数的<code>bitIndex</code>位是<code>0</code>，则结果就是<code>0</code>，以此来判断参数<code>bitIndex</code>存在不。</div>\n<br /><hr /></div>\n</div>\n<a href=\"https://www.jianshu.com/p/4fbad3a6d253\" target=\"_blank\" rel=\"noopener\">点击查看原文</a><br /><br /></div>\n</div>', ' BitSet使用场景和原理', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 318, 6, '2020-02-15 21:06:49', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (74, '面试题04. 二维数组中的查找', '<p>最近leetcode出了剑指offer的题，我们来看下这个题</p>\n<div><code>在一个 n * m 的二维数组中，每一行都按照从左到右递增的顺序排序，每一列都按照从上到下递增的顺序排序。请完成一个函数，输入这样的一个二维数组和一个整数，判断数组中是否含有该整数。</code></div>\n<div>&nbsp;</div>\n<div><code>示例:</code></div>\n<div>&nbsp;</div>\n<div><code>现有矩阵 matrix 如下：</code></div>\n<div>&nbsp;</div>\n<div>\n<div>[</div>\n<div>&nbsp; [1,&nbsp; &nbsp;4,&nbsp; 7, 11, 15],</div>\n<div>&nbsp; [2,&nbsp; &nbsp;5,&nbsp; 8, 12, 19],</div>\n<div>&nbsp; [3,&nbsp; &nbsp;6,&nbsp; 9, 16, 22],</div>\n<div>&nbsp; [10, 13, 14, 17, 24],</div>\n<div>&nbsp; [18, 21, 23, 26, 30]</div>\n<div>]</div>\n<div>&nbsp;</div>\n</div>\n<div><code>给定 target = 5，返回 true。</code></div>\n<div>&nbsp;</div>\n<div><code>给定 target = 20，返回 false。</code></div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div><code>限制：</code></div>\n<div>&nbsp;</div>\n<div><code>0 &lt;= n &lt;= 1000</code></div>\n<div>&nbsp;</div>\n<div><code>0 &lt;= m &lt;= 1000</code></div>\n<hr />\n<h2>分析</h2>\n<p>这题是从给定二维数组里面找到目标值看是否存在，最暴力的方法自然是遍历一遍。然而人家给的数组是有规律的,我们研究下：</p>\n<p><code>每一行都按照从左到右递增的顺序排序，每一列都按照从上到下递增的顺序排序。</code></p>\n<p><code></code><code></code></p>\n<p><code></code></p>\n<p><code></code><code></code></p>\n<p>所以数组左上角，右下角分别是最小，最大的值。</p>\n<p>再看左下角，右上角。</p>\n<p>这两个点是对称的。我们拿左下角举例：<code>如果我们要找的数字大于左下角那么可以直接排除[1，2，3，10，18]一列，如果数字小于左下角排除[18, 21, 23, 26, 30]一行</code>，感觉效率提升飞起啊。</p>\n<p>我们可以从左下角开始设变量x，<code>target&lt;x则排除 x所在行（x是这一行最小的）接着去x上边寻找，target&gt;x则排除x所在列（x是这一列中最大的）接着去x右边寻找。</code></p>\n<p>右上角同理。</p>\n<p>代码选左下角开始寻找：</p>\n<pre class=\"language-java\"><code> public boolean findNumberIn2DArray(int[][] matrix, int target) {\n        if(matrix.length&lt;=0||matrix[0].length&lt;=0)\n            return false;\n\n        int xmax = matrix.length - 1;\n        int ymax = matrix[0].length - 1;\n        if (target &gt; matrix[xmax][ymax] || target &lt; matrix[0][0]) {\n            return false;\n        }\n\n        int i = xmax;\n        for (int j = 0; j &lt;= ymax; j++) {\n            if (target &lt; matrix[i][j]) {//target&gt;x时,x向上移动\n                if(i==0)\n                    return false;\n                i--;\n                j--;\n            }else if(target == matrix[i][j]){\n                return true;\n            }else{ //target&lt;x时,j++,x向右移动\n                continue;\n            }\n           \n        }\n\n        return false;\n    }</code></pre>\n<p><img class=\"wscnph\" src=\"\" /></p>', '在一个 n * m 的二维数组中，每一行都按照从左到右递增的顺序排序，每一列都按照从上到下递增的顺序排序。请完成一个函数，输入这样的一个二维数组和一个整数，判断数组中是否含有该整数。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 300, 2, '2020-02-27 10:49:02', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (75, '面试题14- I. 剪绳子', '<p>给你一根长度为 n 的绳子，请把绳子剪成整数长度的 m 段（m、n都是整数，n&gt;1并且m&gt;1），每段绳子的长度记为 k[0],k[1]...k[m] 。请问 k[0]*k[1]*...*k[m] 可能的最大乘积是多少？</p>\n<p>例如，当绳子的长度是8时，我们把它剪成长度分别为2、3、3的三段，此时得到的最大乘积是18。</p>\n<pre class=\"language-markup\"><code>示例 1：\n\n输入: 2\n输出: 1\n解释: 2 = 1 + 1, 1 &times; 1 = 1</code></pre>\n<pre class=\"language-markup\"><code>示例 2:\n\n输入: 10\n输出: 36\n解释: 10 = 3 + 3 + 4, 3 &times; 3 &times; 4 = 36</code></pre>\n<hr />\n<h2>分析：一.动态规划</h2>\n<p>设&nbsp;<span class=\"katex\"><span class=\"katex-mathml\">F(n) </span></span>为长度为&nbsp;<span class=\"katex\"><span class=\"katex-mathml\">n</span></span>&nbsp;的绳子可以得到的最大乘积，对于每一个&nbsp;<span class=\"katex\"><span class=\"katex-mathml\">F(n)</span></span>，可以得到如下分解：</p>\n<p><img src=\"https://pic.leetcode-cn.com/86e7c3368e2edd8c4bfc907b322204198b56cce7e82e7da9a43a8bbaab50cf9e-14.png\" alt=\"\" width=\"600\" /></p>\n<p>注：我们在1*F（n-1），2*F（n-2）... 这层可以不继续分解，把1*（n-1），（2*n-2）....当作情况之一（我们不必一定到达叶子节点，可以在中间任意一层停止）</p>\n<p>所以状态转移方程dp[i]=Max(1*dp（i-1）,1*（i-1）,2*dp（i-2),（2*i-2）......)</p>\n<pre class=\"language-java\"><code>class Solution {\n  \n    public int cuttingRope(int n) {\n        int[] dp = new int[n + 1];\n        for (int i = 2; i &lt; dp.length; i++) {\n            for (int j = 1; j &lt; i/2+1; j++) {//i/2+1剪枝\n                dp[i]=max3(j*(i-j),j*dp[i-j],dp[i]);\n            }\n        }\n        return dp[n];\n    }\n\n    private int max3(int a, int b, int c) {\n        return Math.max(Math.max(a, b),c);\n    }\n}</code></pre>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<hr />\n<h2>二.贪心法</h2>\n<p>借鉴大佬的方法:<a href=\"https://leetcode-cn.com/problems/jian-sheng-zi-lcof/solution/mian-shi-ti-14-i-jian-sheng-zi-tan-xin-si-xiang-by/\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>\n<p><img src=\"http://sjpeng.top/QQ%E6%88%AA%E5%9B%BE20200229155807.png\" alt=\"\" /></p>\n<p><img src=\"http://sjpeng.top/QQ%E6%88%AA%E5%9B%BE20200229155824.png\" alt=\"\" /></p>\n<pre class=\"language-java\"><code>class Solution {\n      public int cuttingRope(int n) {\n        \n        if(n&lt;=3) return n - 1;\n        int a=n/3;\n        int b = n % 3;\n        if(b==0) return (int) Math.pow(3, a);\n        if(b==1) return (int) Math.pow(3, a - 1) * 4;\n        return (int) Math.pow(3, a) * 2;\n    }\n}</code></pre>', '给你一根长度为 n 的绳子，请把绳子剪成整数长度的 m 段（m、n都是整数，n>1并且m>1），每段绳子的长度记为 k[0],k[1]...k[m] 。请问 k[0]*k[1]*...*k[m] 可能的最大乘积是多少？', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 285, 2, '2020-02-29 15:56:49', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (76, '使用AOP实现一个MyCache注解', '<h2>问题：不改变业务代码的基础上进行增强</h2>\n<p>最近在学习Redis，在Web项目中通过手动判断Redis中是否有对应的key去使用缓存，大概是这样：</p>\n<pre class=\"language-java\"><code>    public List&lt;Student&gt; findStudent() {\n        String key=\"findStudent\";\n        //缓存中有：返回缓存内容\n        if(redisUtil.hasKey(key)){\n            return (List&lt;Student&gt;) redisUtil.get(key);\n        }\n       \n        //缓存中没有：去数据库查询，再存到缓存\n        List&lt;Student&gt; studentList = studentDao.findAll();\n        redisUtil.set(key,studentList);\n        return studentList ;\n    }</code></pre>\n<p>上一个没有缓存前的代码对比下：</p>\n<pre class=\"language-java\"><code>    public List&lt;Student&gt; findStudent() {\n        List&lt;Student&gt; studentList = studentDao.findAll();\n        return studentList ;\n    }</code></pre>\n<p>感觉还是有些麻烦的，我开始考虑有没有办法不改变业务代码并实现缓存的功能。。。果然，天无绝人之路，Spring的AOP完美解决了这个问题。</p>\n<hr />\n<h2>AOP介绍</h2>\n<p>本部分内容出自：<a href=\"https://www.jianshu.com/p/c6d3799f3ab3\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>\n<div>\n<div>\n<p>OP概念：面向切面编程：扩展功能不修改源代码实现</p>\n<p>AOP采用横向抽取机制，取代传统的纵向继承体系重复性代码（性能监视，事务管理，安全检查，缓存）</p>\n<p>（动态代理实现）</p>\n<hr />\n<p>AOP操作术语：</p>\n<p>（JoinPoint）连接点：类里面可以被增强的方法</p>\n<p>（Pointcut）切入点：实际增强的方法，也就是新增的方法</p>\n<p>（Advice）通知/增强：增强的逻辑，比如扩展日志功能，这个日志功能增强</p>\n<p>&nbsp; 前置通知、后置通知、异常通知、最终通知、环绕通知</p>\n<p>（Aspect）切面：把增强应用到具体方法上，过程称为切面</p>\n</div>\n<hr />\n<div>\n<div>\n<p>AOP操作准备</p>\n<p>1.导入AOP相关jar包</p>\n<p>aopalliance-1.0.jar</p>\n<p>aspectjweaver-1.8.7.jar</p>\n<p>spring-aop.RELEASE.jar</p>\n<p>spring-aspects-RELEASE.jar</p>\n<p>2.创建spring核心配置文件，导入aop约束spring-aop.xsd</p>\n<p>3.使用表达式配置切入点</p>\n<p>（1）切入点：实际增强的方法</p>\n<p>（2）常用的表达式</p>\n<p>execution(<strong>方法修饰符 方法返回值 方法所属类 匹配方法名(方法中的形参表)&nbsp; 方法申明抛出的异常</strong>)</p>\n<p>`execution(*空格com.Book.add(..))&nbsp;&nbsp; add()方法</p>\n<p>`execution(*空格com.Book.*(..))&nbsp;&nbsp;&nbsp; 类中的所有方法</p>\n<p>`execution(*空格*.*(..))&nbsp;&nbsp;&nbsp;&nbsp; 所有类中的所有方法</p>\n<p>`execution(*空格save*(..))&nbsp;&nbsp; 匹配所有以save开头的方法名</p>\n<p>&lt;!--配置文件中--&gt;</p>\n<p>&lt;bean id=\"\" class=\"\"&gt;</p>\n<p>&lt;!--配置aop操作--&gt;</p>\n<p>&lt;aop:config&gt;</p>\n<p>&lt;!--配置切入点--&gt;</p>\n<p>&lt;aop:pointcut expression=\"execution(*空格com.aop.Book.*(..)) id=\"pointcut1（切入点名字）\"&gt;</p>\n<p>&lt;!--配置切面&nbsp; 把增强用到方法上--&gt;</p>\n<p>&lt;aop:aspect ref=\"增强类对象的id\" &gt;</p>\n<p>&lt;!--配置增强类型 method:增强类使用在哪个方法上作为前置--&gt;</p>\n<p>&lt;aop:before method=\"before1\" pointcut-ref=\"pointcut1\"/&gt;</p>\n<p>&lt;!--后置通知--&gt;</p>\n<p>&lt;aop:after-returning method=\"after1\" pointcut-ref=\"pointcut1\"/&gt;</p>\n<p>&lt;!--环绕通知--&gt;</p>\n<p>&lt;aop:around method=\"around1\" pointcut-ref=\"pointcut1\"/&gt;</p>\n<p>&lt;/aop:aspect&gt;</p>\n<p>&lt;/aop:config&gt;</p>\n<p>测试：环绕通知</p>\n<p>public void around(ProceedingJoinPoint proceedingJoinPoint){</p>\n<p>//方法之前代码</p>\n<p>//执行要被增强的方法</p>\n<p>proceedingJoinPoint.proceed();</p>\n<p>//方法之后代码</p>\n<p>}</p>\n<hr />\n<h2>开始&nbsp;</h2>\n<p>接下来开始重头戏，自己实现一个MyCache注解。</p>\n<p>步骤如下：1.定义一个注解 2.将注解添加到需要缓存的方法上 3.利用AOP的环绕通知对方法增强</p>\n<p>1.声明一个注解</p>\n<pre class=\"language-java\"><code>@Target(ElementType.METHOD)//注解作用在方法上\n@Retention(RetentionPolicy.RUNTIME)//注解不仅被保存到class文件中，jvm加载class文件之后，仍然存在\npublic @interface MyCache {\n}\n</code></pre>\n</div>\n</div>\n2.在需要增强的方法上加上我们的注解</div>\n<div>\n<pre class=\"language-java\"><code>    @MyCache\n    @Override\n    public List&lt;Tag&gt; findAllPro() {\n        List&lt;Tag&gt; tagList = tagDao.findallTagPro();\n        return tagList;\n    }</code></pre>\n</div>\n<p>3.利用AOP的环绕通知对方法增强</p>\n<pre class=\"language-java\"><code>@Slf4j\n@Aspect\n@Component\npublic class MyCacheAspect {\n    @Autowired\n    private RedisUtil redisUtil;\n\n    //匹配com.peng.service.Impl包下所有带MyCache注解的方法\n    @Around(\"execution(public * com.peng.service.Impl..*(..)) &amp;&amp; @annotation(myCache)\")\n    public Object around(ProceedingJoinPoint jp, MyCache myCache) throws Throwable {\n        long startTime = System.currentTimeMillis();\n        //生成对应Redis的key（生成规则可以自行定义）\n        Signature signature = jp.getSignature();\n        String methodName = signature.getName();\n        String className = signature.getDeclaringTypeName();\n        StringBuffer sbKey = new StringBuffer();\n        sbKey.append(className);\n        sbKey.append(\".\");\n        sbKey.append(methodName);\n        Object[] args = jp.getArgs();//方法参数值\n        for (Object object:args) {\n            sbKey.append(\"-\");\n            sbKey.append(object);\n        }\n        String key=sbKey.toString();\n        //如果有缓存直接返回，没有正常执行并写入缓存\n        try {\n            if (redisUtil.hasKey(key)) {\n                System.out.println(methodName+\"-\"+\"找到缓存了！\");\n                return redisUtil.get(key);\n            } else {\n                System.out.println(methodName+\"----------------没有缓存！\");\n                Object result = jp.proceed(args);\n                redisUtil.set(key, result, 60 * 60);\n                return result;\n            }\n        } catch (Throwable t){\n            log.error(t.toString());\n            return null;\n        } finally {\n            System.out.print(methodName+\"-\"+\"方法执行时间：\");\n            System.out.println(System.currentTimeMillis()-startTime);\n        }\n    }\n}</code></pre>\n<p>执行结果：执行三次 第一次没有缓存 后两次命中缓存</p>\n<pre class=\"language-markup\"><code>findAllPro----------------没有缓存！\nfindAllPro-方法执行时间：506\nfindAllPro-找到缓存了！\nfindAllPro-方法执行时间：5\nfindAllPro-找到缓存了！\nfindAllPro-方法执行时间：3</code></pre>', '1.定义一个注解 2.将注解添加到需要缓存的方法上 3.利用AOP的环绕通知对方法增强', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 365, 6, '2020-03-21 17:59:40', '2020-04-01 17:55:59');
INSERT INTO `t_blog` VALUES (88, '面试题33. 二叉搜索树的后序遍历序列', '<h2>题目要求</h2>\n<h2><img class=\"wscnph\" src=\"http://sjpeng.top/0e3b0adb22ba45edbd88ac143b9f7222.png\" /></h2>\n<p>注意：该题利用搜索树和后续遍历的特性</p>\n<ul>\n<li>&nbsp;&nbsp; &nbsp; &nbsp;二叉搜索树，左子树比根节点小，右子树比根节点大</li>\n<li>&nbsp;&nbsp; &nbsp; &nbsp;后续遍历的最后一个节点是根节点</li>\n</ul>\n<p>所以我们找到该树的所有根节点，判断是否符合左小，右大即可</p>\n<pre class=\"language-java\"><code>class Solution {\n  public boolean verifyPostorder(int[] postorder) {\n        return verify(postorder, 0, postorder.length);\n    }\n    \n    public boolean verify(int[] postorder,int left,int right) {//左闭右开\n\n        int len = right - left;\n        if (len &lt;= 1) {\n            return true;\n        }\n        int root = postorder[right - 1];//找到根节点值\n        int i = left;\n        while (i &lt; right) {//左边小 右边大 找到数组的切分点\n            if (postorder[i] &gt; root) {\n                break;\n            }\n            ++i;\n        }\n        if(i==right) {return verify(postorder,left,i-1);}//root是最大值时没有右子树 去判断左子树是否符合\n        for (int j = i; j &lt; right-1; j++) {//判断是否符合右边大的特性\n            if (postorder[j] &lt;= root) {\n                return false;\n            }\n        }\n\n        //root节点ok。判断root的左右子节点\n        if( verify(postorder,left,i)&amp;&amp; verify(postorder,i,right-1)){\n            return true;\n        }\n        return false;\n    }\n}</code></pre>\n<h2>通过</h2>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/fccb89f003894725974fe922043ff5c0.png\" /></p>', '输入一个整数数组，判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true，否则返回 false。假设输入的数组的任意两个数字都互不相同。', '', 0, 1, 1, 348, 2, '2020-04-02 11:50:43', '2020-07-23 16:38:34');
INSERT INTO `t_blog` VALUES (89, 'Mysql面试常问', '<h2><span class=\"bjh-h3 bjh-text-align-center\"><span class=\"bjh-strong\">事务的ACID特性是什么？</span></span></h2>\n<p>ACID其实是事务特性的英文首字母缩写，具体的含义是这样的：</p>\n<ul>\n<li><span class=\"bjh-ul\"><span class=\"bjh-li\"><span class=\"bjh-p\">原子性（atomicity)一个事务必须被视为一个不可分割的最小工作单元，整个事务中的所有操作要么全部提交成功，要么全部失败回滚，对于一个事务来说，不可能只执行其中的一部分操作。</span></span></span></li>\n<li><span class=\"bjh-ul\"><span class=\"bjh-li\"><span class=\"bjh-p\">一致性（consistency)数据库总是从一个一致性的状态转换到另外一个一致性的状态。在前面的例子中，一致性确保了，即使在执行第三、四条语句之间时系统崩溃，CMBC账户中也不会损失100万，不然lemon要哭死，因为事务最终没有提交，所以事务中所做的修改也不会保存到数据库中。</span></span></span></li>\n<li><span class=\"bjh-ul\"><span class=\"bjh-li\"><span class=\"bjh-p\">隔离性（isolation)通常来说，一个事务所做的修改在最终提交以前，对其他事务是不可见的。在前面的例子中，当执行完第三条语句、第四条语句还未开始时，此时如果有其他人准备给lemon的CMBC账户存钱，那他看到的CMBC账户里还是有100万的。</span></span></span></li>\n<li><span class=\"bjh-ul\"><span class=\"bjh-li\"><span class=\"bjh-p\">持久性（durability)一旦事务提交，则其所做的修改就会永久保存到数据库中。此时即使系统崩溃，修改的数据也不会丢失。持久性是个有点模糊的概念，因为实际上持久性也分很多不同的级别。有些持久性策略能够提供非常强的安全保障，而有些则未必。而且「不可能有能做到100%的持久性保证的策略」否则还需要备份做什么。</span></span></span></li>\n</ul>\n<hr />\n<h2>什么是脏读、不可重复读、幻读？</h2>\n<ul>\n<li><span class=\"bjh-strong\">脏读：</span>在事务A修改数据之后提交数据之前，这时另一个事务B来读取数据，如果不加控制，事务B读取到A修改过数据，之后A又对数据做了修改再提交，则B读到的数据是脏数据，此过程称为脏读Dirty Read。</li>\n<li><span class=\"bjh-strong\">不可重复读：</span>一个事务内在读取某些数据后的某个时间，再次读取以前读过的数据，却发现其读出的数据已经发生了变更、或者某些记录已经被删除了。</li>\n<li><span class=\"bjh-strong\">幻读：</span>事务A在按查询条件读取某个范围的记录时，事务B又在该范围内插入了新的满足条件的记录，当事务A再次按条件查询记录时，会产生新的满足条件的记录（幻行 Phantom Row）</li>\n</ul>\n<hr />\n<h2>四个隔离级别知道吗？解决了什么问题</h2>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/2800079cf2044306bab515bdd844c07c.png\" /></p>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/7b7bf7e8c1c9467394d1da6886b96ce6.png\" /></p>\n<hr />\n<h2>InnoDB与MyISAM对比</h2>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/fba738ba945a4feaae54db822b498624.png\" /></p>\n<hr />\n<h2>MVCC的理解？</h2>\n<p><a href=\"https://baijiahao.baidu.com/s?id=1629409989970483292&amp;wfr=spider&amp;for=pc\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>', '事务的ACID特性是什么？什么是脏读、不可重复读、幻读?四个隔离级别知道吗？解决了什么问题? InnoDB与MyISAM对比?MVCC的理解？', '', 0, 1, 1, 403, 4, '2020-04-03 14:56:33', '2020-04-03 14:56:33');
INSERT INTO `t_blog` VALUES (90, 'Redis集群搭建', '<h3>Reis集群搭建，我们搭建8台服务，一主一从模式。</h3>\n<h3>Redis版本5.0</h3>\n<h3>伪集群部署，8个服务部署在一台服务器，占用不同的端口号。</h3>\n<hr />\n<h3>我们创建8个文件夹，每个放置其端口对应的配置文件</h3>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/6c3eb9df94554a7b82362673bba38dbc.png\" /></p>\n<hr />\n<h2>我们先启动六个服务，配置为3主3从模式</h2>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/ee75cfe3e232417393be54268240c073.png\" width=\"1000\" height=\"600\" /></p>\n<p>&nbsp;</p>\n<h3>1.修改redis.conf文件。（拿7000端口举例，其它7个配置文件修改对应端口号即可）</h3>\n<p><a href=\"http://sjpeng.top/52603c9c617141719ce8254a425643c3.conf\" target=\"_blank\" rel=\"noopener\">查看redis.conf</a></p>\n<pre class=\"language-markup\"><code>port 7000 #端口号\npidfile /var/run/redis_7000.pid #pid文件\ndbfilename dump7000.rdb #指定本地数据库存放目录\ndir /root/myredis/redis7000 #redis数据 + log + pid文件存储的默认目录\nappendfilename \"appendonly7000.aof\" #aof文件名\n------------------------------------------------------------------------------------------\n#放开集群相关配置\ncluster-enabled yes \ncluster-config-file nodes-7000.conf #集群配置文件\ncluster-node-timeout 15000\ncluster-replica-validity-factor 10\ncluster-migration-barrier 1\ncluster-require-full-coverage yes\ncluster-replica-no-failover no</code></pre>\n<h3>2.启动服务，先启动6台</h3>\n<pre class=\"language-markup\"><code>[root@localhost redis-5.0.5]# /usr/local/bin/redis-server /root/myredis/redis7001/redis.conf \n15563:C 07 Apr 2020 17:11:57.167 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\n15563:C 07 Apr 2020 17:11:57.167 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=15563, just started\n15563:C 07 Apr 2020 17:11:57.167 # Configuration loaded\n[root@localhost redis-5.0.5]# /usr/local/bin/redis-server /root/myredis/redis7002/redis.conf \n15568:C 07 Apr 2020 17:12:04.004 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\n15568:C 07 Apr 2020 17:12:04.004 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=15568, just started\n15568:C 07 Apr 2020 17:12:04.004 # Configuration loaded\n[root@localhost redis-5.0.5]# /usr/local/bin/redis-server /root/myredis/redis7003/redis.conf \n15573:C 07 Apr 2020 17:12:09.332 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\n15573:C 07 Apr 2020 17:12:09.332 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=15573, just started\n15573:C 07 Apr 2020 17:12:09.332 # Configuration loaded\n[root@localhost redis-5.0.5]# /usr/local/bin/redis-server /root/myredis/redis7004/redis.conf \n15578:C 07 Apr 2020 17:12:15.757 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\n15578:C 07 Apr 2020 17:12:15.757 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=15578, just started\n15578:C 07 Apr 2020 17:12:15.757 # Configuration loaded\n[root@localhost redis-5.0.5]# /usr/local/bin/redis-server /root/myredis/redis7005/redis.conf \n15583:C 07 Apr 2020 17:12:19.350 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\n15583:C 07 Apr 2020 17:12:19.350 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=15583, just started\n15583:C 07 Apr 2020 17:12:19.350 # Configuration loaded</code></pre>\n<p>查看服务状态</p>\n<pre class=\"language-markup\"><code>[root@localhost redis-5.0.5]# ps -ef|grep redis\nroot      15559      1  0 17:11 ?        00:00:00 /usr/local/bin/redis-server 192.168.21.3:7000 [cluster]\nroot      15564      1  0 17:11 ?        00:00:00 /usr/local/bin/redis-server 192.168.21.3:7001 [cluster]\nroot      15569      1  0 17:12 ?        00:00:00 /usr/local/bin/redis-server 192.168.21.3:7002 [cluster]\nroot      15574      1  0 17:12 ?        00:00:00 /usr/local/bin/redis-server 192.168.21.3:7003 [cluster]\nroot      15579      1  0 17:12 ?        00:00:00 /usr/local/bin/redis-server 192.168.21.3:7004 [cluster]\nroot      15584      1  0 17:12 ?        00:00:00 /usr/local/bin/redis-server 192.168.21.3:7005 [cluster]</code></pre>\n<p>（[cluster]说明以集群方式运行）</p>\n<h3>3.分配插槽，以及主从配置（ --cluster-replicas 1 [这里的1是从节点数/主节点数的比例][3主3从]）</h3>\n<pre class=\"language-markup\"><code>[root@localhost redis-5.0.5]# /usr/local/bin/redis-cli --cluster create 192.168.21.3:7000 192.168.21.3:7001 192.168.21.3:7002 192.168.21.3:7003 192.168.21.3:7004 192.168.21.3:7005 --cluster-replicas 1</code></pre>\n<h3>4.成功执行set命令（这里注意 加-c）</h3>\n<pre class=\"language-markup\"><code>[root@localhost /]# /usr/local/bin/redis-cli -h 192.168.21.3 -p 7000 -c\n192.168.21.3:7000&gt; set k1 v1\n-&gt; Redirected to slot [12706] located at 192.168.21.3:7002\nOK\n</code></pre>\n<p>&nbsp;</p>\n<hr />\n<p>&nbsp;注：查看集群内节点信息</p>\n<pre class=\"language-markup\"><code>192.168.21.3:7000&gt; cluster nodes\n7676b8930bc7f04ffb56f425cc0090c0445e4a58 192.168.21.3:7003@17003 slave 508d1f4c7af0a395690a48a4a9953815e6b63812 0 1586251695000 4 connected\n3d512efae957b73879a09118cb43229f42762f69 192.168.21.3:7005@17005 slave 8aa2622ea39cb2052ba378ffe66df641bb00b31d 0 1586251699000 6 connected\n59ab0b13274df67271d0448c2a732f398d866271 192.168.21.3:7004@17004 slave d93b319b50616a129e3979539c0ec7c103c0468f 0 1586251700626 5 connected\nd93b319b50616a129e3979539c0ec7c103c0468f 192.168.21.3:7002@17002 master - 0 1586251700000 3 connected 10923-16383\n8aa2622ea39cb2052ba378ffe66df641bb00b31d 192.168.21.3:7000@17000 myself,master - 0 1586251698000 1 connected 0-5460\n508d1f4c7af0a395690a48a4a9953815e6b63812 192.168.21.3:7001@17001 master - 0 1586251698608 2 connected 5461-10922\n</code></pre>\n<p>&nbsp;集群帮助命令</p>\n<pre class=\"language-markup\"><code>[root@localhost redis-5.0.5]# /usr/local/bin/redis-cli --cluster help\nCluster Manager Commands:\n  create         host1:port1 ... hostN:portN\n                 --cluster-replicas &lt;arg&gt;\n  check          host:port\n                 --cluster-search-multiple-owners\n  info           host:port\n  fix            host:port\n                 --cluster-search-multiple-owners\n  reshard        host:port\n                 --cluster-from &lt;arg&gt;\n                 --cluster-to &lt;arg&gt;\n                 --cluster-slots &lt;arg&gt;\n                 --cluster-yes\n                 --cluster-timeout &lt;arg&gt;\n                 --cluster-pipeline &lt;arg&gt;\n                 --cluster-replace\n  rebalance      host:port\n                 --cluster-weight &lt;node1=w1...nodeN=wN&gt;\n                 --cluster-use-empty-masters\n                 --cluster-timeout &lt;arg&gt;\n                 --cluster-simulate\n                 --cluster-pipeline &lt;arg&gt;\n                 --cluster-threshold &lt;arg&gt;\n                 --cluster-replace\n  add-node       new_host:new_port existing_host:existing_port\n                 --cluster-slave\n                 --cluster-master-id &lt;arg&gt;\n  del-node       host:port node_id\n  call           host:port command arg arg .. arg\n  set-timeout    host:port milliseconds\n  import         host:port\n                 --cluster-from &lt;arg&gt;\n                 --cluster-copy\n                 --cluster-replace\n  help       </code></pre>\n<hr />\n<h2>新增两台服务</h2>\n<h3>1.将剩下两台服务启动</h3>\n<pre class=\"language-markup\"><code>[root@localhost redis-5.0.5]# /usr/local/bin/redis-server /root/myredis/redis7006/redis.conf \n15634:C 07 Apr 2020 18:25:17.324 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\n15634:C 07 Apr 2020 18:25:17.324 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=15634, just started\n15634:C 07 Apr 2020 18:25:17.324 # Configuration loaded\n[root@localhost redis-5.0.5]# /usr/local/bin/redis-server /root/myredis/redis7007/redis.conf \n15639:C 07 Apr 2020 18:25:21.891 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo\n15639:C 07 Apr 2020 18:25:21.891 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=15639, just started\n15639:C 07 Apr 2020 18:25:21.891 # Configuration loaded\n</code></pre>\n<h3>&nbsp;2.将7006加入集群并设置为master节点</h3>\n<pre class=\"language-markup\"><code>[root@localhost /]# /usr/local/bin/redis-cli --cluster add-node 192.168.21.3:7006 192.168.21.3:7000</code></pre>\n<pre class=\"language-markup\"><code>&gt;&gt;&gt; Adding node 192.168.21.3:7006 to cluster 192.168.21.3:7000\n&gt;&gt;&gt; Performing Cluster Check (using node 192.168.21.3:7000)\nM: 8aa2622ea39cb2052ba378ffe66df641bb00b31d 192.168.21.3:7000\n   slots:[0-5460] (5461 slots) master\n   1 additional replica(s)\nS: 7676b8930bc7f04ffb56f425cc0090c0445e4a58 192.168.21.3:7003\n   slots: (0 slots) slave\n   replicates 508d1f4c7af0a395690a48a4a9953815e6b63812\nS: 3d512efae957b73879a09118cb43229f42762f69 192.168.21.3:7005\n   slots: (0 slots) slave\n   replicates 8aa2622ea39cb2052ba378ffe66df641bb00b31d\nS: 59ab0b13274df67271d0448c2a732f398d866271 192.168.21.3:7004\n   slots: (0 slots) slave\n   replicates d93b319b50616a129e3979539c0ec7c103c0468f\nM: d93b319b50616a129e3979539c0ec7c103c0468f 192.168.21.3:7002\n   slots:[10923-16383] (5461 slots) master\n   1 additional replica(s)\nM: 508d1f4c7af0a395690a48a4a9953815e6b63812 192.168.21.3:7001\n   slots:[5461-10922] (5462 slots) master\n   1 additional replica(s)\n[OK] All nodes agree about slots configuration.\n&gt;&gt;&gt; Check for open slots...\n&gt;&gt;&gt; Check slots coverage...\n[OK] All 16384 slots covered.\n&gt;&gt;&gt; Send CLUSTER MEET to node 192.168.21.3:7006 to make it join the cluster.\n[OK] New node added correctly.</code></pre>\n<h3>3.将7007加入集群并设置为从节点</h3>\n<p>1.需要先找到7006节点的ID</p>\n<pre class=\"language-markup\"><code>[root@localhost /]# /usr/local/bin/redis-cli -h 192.168.21.3 -p 7000\n192.168.21.3:7000&gt; cluster nodes\n7676b8930bc7f04ffb56f425cc0090c0445e4a58 192.168.21.3:7003@17003 slave 508d1f4c7af0a395690a48a4a9953815e6b63812 0 1586255603196 4 connected\n3d512efae957b73879a09118cb43229f42762f69 192.168.21.3:7005@17005 slave 8aa2622ea39cb2052ba378ffe66df641bb00b31d 0 1586255602000 6 connected\n59ab0b13274df67271d0448c2a732f398d866271 192.168.21.3:7004@17004 slave d93b319b50616a129e3979539c0ec7c103c0468f 0 1586255603000 5 connected\n4bc9e9939ede8239f8e53527527916d3cf0b393d 192.168.21.3:7006@17006 master - 0 1586255605211 0 connected\n</code></pre>\n<p>2.7007加入集群并设置为7006的从节点</p>\n<pre class=\"language-markup\"><code>[root@localhost /]# /usr/local/bin/redis-cli --cluster add-node 192.168.21.3:7007 192.168.21.3:7000 --cluster-slave --cluster-master-id 4bc9e9939ede8239f8e53527527916d3cf0b393d\n</code></pre>\n<pre class=\"language-markup\"><code>&gt;&gt;&gt; Adding node 192.168.21.3:7007 to cluster 192.168.21.3:7000\n&gt;&gt;&gt; Performing Cluster Check (using node 192.168.21.3:7000)\nM: 8aa2622ea39cb2052ba378ffe66df641bb00b31d 192.168.21.3:7000\n   slots:[0-5460] (5461 slots) master\n   1 additional replica(s)\nS: 7676b8930bc7f04ffb56f425cc0090c0445e4a58 192.168.21.3:7003\n   slots: (0 slots) slave\n   replicates 508d1f4c7af0a395690a48a4a9953815e6b63812\nS: 3d512efae957b73879a09118cb43229f42762f69 192.168.21.3:7005\n   slots: (0 slots) slave\n   replicates 8aa2622ea39cb2052ba378ffe66df641bb00b31d\nS: 59ab0b13274df67271d0448c2a732f398d866271 192.168.21.3:7004\n   slots: (0 slots) slave\n   replicates d93b319b50616a129e3979539c0ec7c103c0468f\nM: 4bc9e9939ede8239f8e53527527916d3cf0b393d 192.168.21.3:7006\n   slots: (0 slots) master\nM: d93b319b50616a129e3979539c0ec7c103c0468f 192.168.21.3:7002\n   slots:[10923-16383] (5461 slots) master\n   1 additional replica(s)\nM: 508d1f4c7af0a395690a48a4a9953815e6b63812 192.168.21.3:7001\n   slots:[5461-10922] (5462 slots) master\n   1 additional replica(s)\n[OK] All nodes agree about slots configuration.\n&gt;&gt;&gt; Check for open slots...\n&gt;&gt;&gt; Check slots coverage...\n[OK] All 16384 slots covered.\n&gt;&gt;&gt; Send CLUSTER MEET to node 192.168.21.3:7007 to make it join the cluster.\nWaiting for the cluster to join\n\n&gt;&gt;&gt; Configure node as replica of 192.168.21.3:7006.\n[OK] New node added correctly.</code></pre>\n<p>查看集群中节点，有8个了</p>\n<pre class=\"language-markup\"><code>192.168.21.3:7000&gt; cluster nodes\n7676b8930bc7f04ffb56f425cc0090c0445e4a58 192.168.21.3:7003@17003 slave 508d1f4c7af0a395690a48a4a9953815e6b63812 0 1586255942000 4 connected\n3d512efae957b73879a09118cb43229f42762f69 192.168.21.3:7005@17005 slave 8aa2622ea39cb2052ba378ffe66df641bb00b31d 0 1586255944826 6 connected\n59ab0b13274df67271d0448c2a732f398d866271 192.168.21.3:7004@17004 slave d93b319b50616a129e3979539c0ec7c103c0468f 0 1586255943000 5 connected\n4bc9e9939ede8239f8e53527527916d3cf0b393d 192.168.21.3:7006@17006 master - 0 1586255940795 0 connected\nd93b319b50616a129e3979539c0ec7c103c0468f 192.168.21.3:7002@17002 master - 0 1586255941000 3 connected 10923-16383\n8aa2622ea39cb2052ba378ffe66df641bb00b31d 192.168.21.3:7000@17000 myself,master - 0 1586255942000 1 connected 0-5460\n0cc2b08fb51dcfb9373dfa7c2919eab12b2dfa8a 192.168.21.3:7007@17007 slave 4bc9e9939ede8239f8e53527527916d3cf0b393d 0 1586255942812 0 connected\n508d1f4c7af0a395690a48a4a9953815e6b63812 192.168.21.3:7001@17001 master - 0 1586255943819 2 connected 5461-10922\n</code></pre>\n<h3>4.重新分配集群槽位</h3>\n<pre class=\"language-markup\"><code>[root@localhost /]# /usr/local/bin/redis-cli --cluster reshard 192.168.21.3:7000</code></pre>\n<p>&nbsp;分配多少个槽位？</p>\n<pre class=\"language-markup\"><code>How many slots do you want to move (from 1 to 16384)? 4096\n</code></pre>\n<p>&nbsp;分配给哪个节点？（选择7006的节点ID）</p>\n<pre class=\"language-markup\"><code>What is the receiving node ID? 4bc9e9939ede8239f8e53527527916d3cf0b393d\n</code></pre>\n<p>&nbsp;分配完成后，查看集群节点信息</p>\n<pre class=\"language-markup\"><code>192.168.21.3:7000&gt; cluster nodes\n7676b8930bc7f04ffb56f425cc0090c0445e4a58 192.168.21.3:7003@17003 slave 508d1f4c7af0a395690a48a4a9953815e6b63812 0 1586256527217 4 connected\n3d512efae957b73879a09118cb43229f42762f69 192.168.21.3:7005@17005 slave 8aa2622ea39cb2052ba378ffe66df641bb00b31d 0 1586256525203 6 connected\n59ab0b13274df67271d0448c2a732f398d866271 192.168.21.3:7004@17004 slave d93b319b50616a129e3979539c0ec7c103c0468f 0 1586256525000 5 connected\n4bc9e9939ede8239f8e53527527916d3cf0b393d 192.168.21.3:7006@17006 master - 0 1586256524000 8 connected 0-1364 5461-6826 10923-12287\nd93b319b50616a129e3979539c0ec7c103c0468f 192.168.21.3:7002@17002 master - 0 1586256522000 3 connected 12288-16383\n8aa2622ea39cb2052ba378ffe66df641bb00b31d 192.168.21.3:7000@17000 myself,master - 0 1586256518000 1 connected 1365-5460\n0cc2b08fb51dcfb9373dfa7c2919eab12b2dfa8a 192.168.21.3:7007@17007 slave 4bc9e9939ede8239f8e53527527916d3cf0b393d 0 1586256526210 8 connected\n508d1f4c7af0a395690a48a4a9953815e6b63812 192.168.21.3:7001@17001 master - 0 1586256524000 2 connected 6827-10922\n</code></pre>\n<p>&nbsp;完成！</p>', 'Redis集群搭建', '', 0, 1, 1, 400, 7, '2020-04-07 17:52:16', '2020-04-07 17:52:16');
INSERT INTO `t_blog` VALUES (91, 'redis的三种集群方式', '<h1 class=\"postTitle\">redis的三种集群方式</h1>\n<div class=\"postBody\">\n<div id=\"cnblogs_post_body\" class=\"blogpost-body \">\n<p>redis有三种集群方式：主从复制，哨兵模式和集群。</p>\n<h2><strong>1.主从复制</strong></h2>\n<p><strong>主从复制原理：</strong></p>\n<ul>\n<li>从服务器连接主服务器，发送SYNC命令；&nbsp;</li>\n<li>主服务器接收到SYNC命名后，开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令；&nbsp;</li>\n<li>主服务器BGSAVE执行完后，向所有从服务器发送快照文件，并在发送期间继续记录被执行的写命令；&nbsp;</li>\n<li>从服务器收到快照文件后丢弃所有旧数据，载入收到的快照；&nbsp;</li>\n<li>主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令；&nbsp;</li>\n<li>从服务器完成对快照的载入，开始接收命令请求，并执行来自主服务器缓冲区的写命令；（<strong>从服务器初始化完成</strong>）</li>\n<li>主服务器每执行一个写命令就会向从服务器发送相同的写命令，从服务器接收并执行收到的写命令（<strong>从服务器初始化完成后的操作</strong>）</li>\n</ul>\n<p><strong>主从复制优缺点：</strong></p>\n<p><strong>优点：</strong></p>\n<ul>\n<li>支持主从复制，主机会自动将数据同步到从机，可以进行读写分离</li>\n<li>为了分载Master的读操作压力，Slave服务器可以为客户端提供只读操作的服务，写服务仍然必须由Master来完成</li>\n<li>Slave同样可以接受其它Slaves的连接和同步请求，这样可以有效的分载Master的同步压力。</li>\n<li>Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间，客户端仍然可以提交查询或修改请求。</li>\n<li>Slave Server同样是以非阻塞的方式完成数据同步。在同步期间，如果有客户端提交查询请求，Redis则返回同步之前的数据</li>\n</ul>\n<p><strong>缺点：</strong></p>\n<ul>\n<li>Redis不具备自动容错和恢复功能，主机从机的宕机都会导致前端部分读写请求失败，需要等待机器重启或者手动切换前端的IP才能恢复。</li>\n<li>主机宕机，宕机前有部分数据未能及时同步到从机，切换IP后还会引入数据不一致的问题，降低了系统的可用性。</li>\n<li>Redis较难支持在线扩容，在集群容量达到上限时在线扩容会变得很复杂。</li>\n</ul>\n<h2><strong>2.哨兵模式</strong></h2>\n<p>当主服务器中断服务后，可以将一个从服务器升级为主服务器，以便继续提供服务，但是这个过程需要人工手动来操作。&nbsp;为此，Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。</p>\n<p>哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个。</p>\n<p>&nbsp;&nbsp;&nbsp; （1）监控主服务器和从服务器是否正常运行。&nbsp;<br />&nbsp;&nbsp;&nbsp; （2）主服务器出现故障时自动将从服务器转换为主服务器。</p>\n<p><strong>哨兵的工作方式：</strong></p>\n<ul>\n<li>每个Sentinel（哨兵）进程以每秒钟一次的频率向整个集群中的Master主服务器，Slave从服务器以及其他Sentinel（哨兵）进程发送一个 PING 命令。</li>\n<li>如果一个实例（instance）距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值， 则这个实例会被 Sentinel（哨兵）进程标记为主观下线（SDOWN）</li>\n<li>如果一个Master主服务器被标记为主观下线（SDOWN），则正在监视这个Master主服务器的所有 Sentinel（哨兵）进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态</li>\n<li>当有足够数量的 Sentinel（哨兵）进程（大于等于配置文件指定的值）在指定的时间范围内确认Master主服务器进入了主观下线状态（SDOWN）， 则Master主服务器会被标记为客观下线（ODOWN）</li>\n<li>在一般情况下， 每个 Sentinel（哨兵）进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。</li>\n<li>当Master主服务器被 Sentinel（哨兵）进程标记为客观下线（ODOWN）时，Sentinel（哨兵）进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。</li>\n<li>若没有足够数量的 Sentinel（哨兵）进程同意 Master主服务器下线， Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel（哨兵）进程发送 PING 命令返回有效回复，Master主服务器的主观下线状态就会被移除。</li>\n</ul>\n<p>&nbsp;<strong>哨兵模式的优缺点</strong></p>\n<p><strong>优点：</strong></p>\n<ul>\n<li>哨兵模式是基于主从模式的，所有主从的优点，哨兵模式都具有。</li>\n<li>主从可以自动切换，系统更健壮，可用性更高。</li>\n</ul>\n<p><strong>缺点：</strong></p>\n<ul>\n<li>Redis较难支持在线扩容，在集群容量达到上限时在线扩容会变得很复杂。</li>\n</ul>\n<h2 id=\"哨兵模式的优缺点\"><strong>3.Redis-Cluster集群</strong></h2>\n<p>redis的哨兵模式基本已经可以实现高可用，读写分离 ，但是在这种模式下每台redis服务器都存储相同的数据，很浪费内存，所以在redis3.0上加入了cluster模式，实现的redis的分布式存储，也就是说每台redis节点上存储不同的内容。</p>\n<p>&nbsp;Redis-Cluster采用无中心结构,它的特点如下：</p>\n<ul>\n<li>\n<p>所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。</p>\n</li>\n<li>\n<p>节点的fail是通过集群中超过半数的节点检测失效时才生效。</p>\n</li>\n<li>\n<p>客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。</p>\n</li>\n</ul>\n<p><strong>工作方式：</strong></p>\n<p>在redis的每一个节点上，都有这么两个东西，一个是插槽（slot），它的的取值范围是：0-16383。还有一个就是cluster，可以理解为是一个集群管理的插件。当我们的存取的key到达的时候，redis会根据crc16的算法得出一个结果，然后把结果对 16384 求余数，这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽，通过这个值，去找到对应的插槽所对应的节点，然后直接自动跳转到这个对应的节点上进行存取操作。</p>\n<p>为了保证高可用，redis-cluster集群引入了主从模式，一个主节点对应一个或者多个从节点，当主节点宕机的时候，就会启用从节点。当其它主节点ping一个主节点A时，如果半数以上的主节点与A通信超时，那么认为主节点A宕机了。如果主节点A和它的从节点A1都宕机了，那么该集群就无法再提供服务了。</p>\n</div>\n</div>', 'redis的三种集群方式', '', 0, 1, 1, 404, 7, '2020-04-07 19:00:53', '2020-04-07 19:00:53');
INSERT INTO `t_blog` VALUES (92, 'LockSupport（park/unpark）源码分析', '<div>\n<h2>LockSupport（park/unpark）源码分析</h2>\n<p><a href=\"https://www.jianshu.com/p/e3afe8ab8364\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n<p>concurrent包是基于AQS (AbstractQueuedSynchronizer)框架的，AQS框架借助于两个类：</p>\n<ul>\n<li>Unsafe（提供CAS操作）</li>\n<li>LockSupport（提供park/unpark操作）</li>\n</ul>\n<p>因此，LockSupport非常重要。</p>\n<h2>两个重点</h2>\n<p>（1）<strong>操作对象</strong></p>\n<p>归根结底，LockSupport.park()和LockSupport.unpark(Thread thread)调用的是Unsafe中的native代码：</p>\n</div>\n<div>\n<div>\n<div class=\"_2Uzcx_\">\n<pre class=\"line-numbers  language-csharp\"><code>//LockSupport中\npublic static void unpark(Thread thread) {\n        if (thread != null)\n            UNSAFE.unpark(thread);\n    }\n</code></pre>\n</div>\n<p><strong>Unsafe类中的对应方法：</strong></p>\n<div class=\"_2Uzcx_\">\n<pre class=\"line-numbers  language-java\"><code>    //park\n    public native void park(boolean isAbsolute, long time);\n    \n    //unpack\n    public native void unpark(Object var1);\n</code></pre>\n</div>\n<p>park函数是将当前调用Thread阻塞，而unpark函数则是将指定线程Thread唤醒。</p>\n<p><strong>与Object类的wait/notify机制相比，park/unpark有两个优点：</strong></p>\n<ul>\n<li>以thread为操作对象更符合阻塞线程的直观定义</li>\n<li>操作更精准，可以准确地唤醒某一个线程（notify随机唤醒一个线程，notifyAll唤醒所有等待的线程），增加了灵活性。</li>\n</ul>\n<p><strong>（2）关于&ldquo;许可&rdquo;</strong></p>\n<p>在上面的文字中，我使用了阻塞和唤醒，是为了和wait/notify做对比。</p>\n<ul>\n<li>\n<p><strong>其实park/unpark的设计原理核心是&ldquo;许可&rdquo;：park是等待一个许可，unpark是为某线程提供一个许可。</strong><br />如果某线程A调用park，那么除非另外一个线程调用unpark(A)给A一个许可，否则线程A将阻塞在park操作上。</p>\n</li>\n<li>\n<p><strong>有一点比较难理解的，是unpark操作可以再park操作之前。</strong><br />也就是说，先提供许可。当某线程调用park时，已经有许可了，它就消费这个许可，然后可以继续运行。这其实是必须的。考虑最简单的生产者(Producer)消费者(Consumer)模型：Consumer需要消费一个资源，于是调用park操作等待；Producer则生产资源，然后调用unpark给予Consumer使用的许可。非常有可能的一种情况是，Producer先生产，这时候Consumer可能还没有构造好（比如线程还没启动，或者还没切换到该线程）。那么等Consumer准备好要消费时，显然这时候资源已经生产好了，可以直接用，那么park操作当然可以直接运行下去。如果没有这个语义，那将非常难以操作。</p>\n</li>\n<li>\n<p><strong>但是这个&ldquo;许可&rdquo;是不能叠加的，&ldquo;许可&rdquo;是一次性的。</strong><br />比如线程B连续调用了三次unpark函数，当线程A调用park函数就使用掉这个&ldquo;许可&rdquo;，如果线程A再次调用park，则进入等待状态。</p>\n</li>\n</ul>\n<h2>Unsafe.park和Unsafe.unpark的底层实现原理</h2>\n<p>在Linux系统下，是用的Posix线程库pthread中的mutex（互斥量），condition（条件变量）来实现的。<br /><strong>mutex和condition保护了一个_counter的变量，当park时，这个变量被设置为0，当unpark时，这个变量被设置为1。</strong></p>\n<p>源码：<br />每个Java线程都有一个Parker实例，Parker类是这样定义的：</p>\n<div class=\"_2Uzcx_\">\n<pre class=\"line-numbers  language-cpp\"><code>class Parker : public os::PlatformParker {  \nprivate:  \n  volatile int _counter ;  \n  ...  \npublic:  \n  void park(bool isAbsolute, jlong time);  \n  void unpark();  \n  ...  \n}  \nclass PlatformParker : public CHeapObj&lt;mtInternal&gt; {  \n  protected:  \n    pthread_mutex_t _mutex [1] ;  \n    pthread_cond_t  _cond  [1] ;  \n    ...  \n}  \n</code></pre>\n</div>\n<p>可以看到Parker类实际上用Posix的mutex，condition来实现的。<br />在Parker类里的_counter字段，就是用来记录&ldquo;许可&rdquo;的。</p>\n<ul>\n<li><strong>park 过程</strong></li>\n</ul>\n<p>当调用park时，先尝试能否直接拿到&ldquo;许可&rdquo;，即_counter&gt;0时，如果成功，则把_counter设置为0，并返回：</p>\n<div class=\"_2Uzcx_\">\n<pre class=\"line-numbers  language-cpp\"><code>void Parker::park(bool isAbsolute, jlong time) {  \n  \n  // Ideally we\'d do something useful while spinning, such  \n  // as calling unpackTime().  \n  \n  // Optional fast-path check:  \n  // Return immediately if a permit is available.  \n  // We depend on Atomic::xchg() having full barrier semantics  \n  // since we are doing a lock-free update to _counter.  \n  \n  if (Atomic::xchg(0, &amp;_counter) &gt; 0) return;  \n</code></pre>\n</div>\n<p>如果不成功，则构造一个ThreadBlockInVM，然后检查_counter是不是&gt;0，如果是，则把_counter设置为0，unlock mutex并返回：</p>\n<div class=\"_2Uzcx_\">\n<pre class=\"line-numbers  language-cpp\"><code>ThreadBlockInVM tbivm(jt);  \nif (_counter &gt; 0)  { // no wait needed  \n  _counter = 0;  \n  status = pthread_mutex_unlock(_mutex);  \n</code></pre>\n</div>\n<p>否则，再判断等待的时间，然后再调用pthread_cond_wait函数等待，如果等待返回，则把_counter设置为0，unlock mutex并返回：</p>\n<div class=\"_2Uzcx_\">\n<pre class=\"line-numbers  language-cpp\"><code>if (time == 0) {  \n  status = pthread_cond_wait (_cond, _mutex) ;  \n}  \n_counter = 0 ;  \nstatus = pthread_mutex_unlock(_mutex) ;  \nassert_status(status == 0, status, \"invariant\") ;  \nOrderAccess::fence();  \n</code></pre>\n</div>\n<ul>\n<li><strong>unpark 过程</strong></li>\n</ul>\n<p>当unpark时，则简单多了，直接设置_counter为1，再unlock mutex返回。如果_counter之前的值是0，则还要调用pthread_cond_signal唤醒在park中等待的线程：</p>\n<div class=\"_2Uzcx_\">\n<pre class=\"line-numbers  language-dart\"><code>void Parker::unpark() {  \n  int s, status ;  \n  status = pthread_mutex_lock(_mutex);  \n  assert (status == 0, \"invariant\") ;  \n  s = _counter;  \n  _counter = 1;  \n  if (s &lt; 1) {  \n     if (WorkAroundNPTLTimedWaitHang) {  \n        status = pthread_cond_signal (_cond) ;  \n        assert (status == 0, \"invariant\") ;  \n        status = pthread_mutex_unlock(_mutex);  \n        assert (status == 0, \"invariant\") ;  \n     } else {  \n        status = pthread_mutex_unlock(_mutex);  \n        assert (status == 0, \"invariant\") ;  \n        status = pthread_cond_signal (_cond) ;  \n        assert (status == 0, \"invariant\") ;  \n     }  \n  } else {  \n    pthread_mutex_unlock(_mutex);  \n    assert (status == 0, \"invariant\") ;  \n  }  \n}  </code></pre>\n</div>\n</div>\n<br /><br /><br /></div>', 'LockSupport类是Java6(JSR166-JUC)引入的一个类，提供了基本的线程同步原语。', '', 0, 1, 1, 363, 1, '2020-05-11 12:44:52', '2020-05-11 12:44:52');
INSERT INTO `t_blog` VALUES (93, 'Java并发之AQS详解', '<p><a href=\"https://www.cnblogs.com/waterystone/p/4920797.html\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '抽象的队列式的同步器，AQS定义了一套多线程访问共享资源的同步器框架，许多同步类实现都依赖于它，如常用的ReentrantLock/Semaphore/CountDownLatch...。', '', 0, 1, 1, 485, 1, '2020-05-11 13:56:10', '2020-05-11 13:56:10');
INSERT INTO `t_blog` VALUES (95, 'mysql explain type的区别和性能优化', '<p><a href=\"https://blog.csdn.net/lilongsy/article/details/95184594\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>', '在日常工作中，我们会有时会开慢查询去记录一些执行时间比较久的SQL语句，找出这些SQL语句并不意味着完事了，些时我们常常用到explain这个命令来查看一个这些SQL语句的执行计划', '', 0, 1, 1, 287, 4, '2020-07-08 15:26:11', '2020-07-08 15:26:11');
INSERT INTO `t_blog` VALUES (96, '131分割回文串', '<h2>131. 分割回文串</h2>\n<pre><br />//给定一个字符串 s，将 s 分割成一些子串，使每个子串都是回文串。 <br />//<br />// 返回 s 所有可能的分割方案。 <br />//<br />// 示例: <br />//<br />// 输入:&nbsp;\"aab\"<br />//输出:<br />//[<br />//  [\"aa\",\"b\"],<br />//  [\"a\",\"a\",\"b\"]<br />//] <br />// Related Topics 回溯算法 </pre>\n<hr />\n<p>思路：先用dp记录字符串(i-j)是否是回文串，之后回溯将所有的可能记录</p>\n<pre class=\"language-java\"><code>class Solution {\n    List&lt;List&lt;String&gt;&gt; result = null;\n    boolean[][] dp = null;\n\n    public List&lt;List&lt;String&gt;&gt; partition(String s) {\n        char[] chars = s.toCharArray();\n        dp = new boolean[chars.length][chars.length];\n        for (int i = dp.length - 1; i &gt;= 0; i--) {\n            for (int j = i; j &lt; dp[0].length; j++) {\n                if (i == j) {\n                    dp[i][j] = true;\n                    continue;\n                }\n                if (j - 1 == i) {\n                    if (chars[i] == chars[j]) {\n                        dp[i][j] = true;\n                    }\n                } else {\n                    if (chars[i] == chars[j]) {\n                        dp[i][j] = dp[i + 1][j - 1];\n                    }\n                }\n\n            }\n        }\n        result = new ArrayList&lt;&gt;();\n        backStack(s, 0, new ArrayList&lt;&gt;());\n        return result;\n    }\n\n\n    private void backStack(String s, int i, List&lt;String&gt; list) {\n        if (i == s.length()) {\n            result.add(new ArrayList&lt;&gt;(list));\n            return;\n        }\n        for (int j = i; j &lt; s.length(); j++) {\n            if (dp[i][j]) {\n                list.add(s.substring(i, j + 1));\n                backStack(s, j+1, list);\n                list.remove(list.size() - 1);\n            }\n        }\n    }\n\n</code></pre>\n<hr />\n<h2>Result:</h2>\n<p>解答成功:<br />执行耗时:2 ms,击败了98.47% 的Java用户<br />内存消耗:40 MB,击败了47.06% 的Java用户</p>', '给定一个字符串 s，将 s 分割成一些子串，使每个子串都是回文串。 ', '', 0, 1, 1, 326, 2, '2020-07-23 16:34:09', '2020-07-23 16:34:09');
INSERT INTO `t_blog` VALUES (97, '用Map+函数式接口来实现策略模式', '<p><a href=\"https://www.cnblogs.com/keeya/p/13187727.html\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>', '用Map+函数式接口来实现策略模式', '', 0, 1, 1, 302, 6, '2020-07-23 18:00:34', '2020-07-23 18:00:34');
INSERT INTO `t_blog` VALUES (98, 'JRebel插件使用详解', '<p><a href=\"https://blog.csdn.net/lianghecai52171314/article/details/105637251\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>', 'JRebel是一款JAVA虚拟机插件，它使得JAVA程序员能在不进行重部署的情况下，即时看到代码的改变对一个应用程序带来的影响。', '', 0, 1, 1, 427, 6, '2020-07-23 19:26:41', '2020-07-23 19:26:41');
INSERT INTO `t_blog` VALUES (99, 'count(*) 这么慢，我该怎么办？', '<div><span>mysql常用的两种引擎 Mysalm和InnoDB</span></div>\n<div><span>Mysalm引擎把一个表的总行数存在磁盘上，因此执行 count(*)&nbsp;可以立即返回，效率很高</span></div>\n<div><span>InnoDB引擎需要把数据行一行一行读出来，统计计数。（由于多版本并发控制MVCC，InnoDB对于返回多少条记录是不确定的，可见的行才能够用于计算&ldquo;基于这个查询&rdquo;的表的总行数）</span></div>\n<div><span>&nbsp;</span></div>\n<div><span>count(*)的优化，因为主键索引的叶子节点是真实数据，普通索引的叶子节点是主键值，所以普通索引树比主键索引树小很多。对于 count(*)操作，遍历哪个索引树得到的结果都是一样的。因此，MySQL优化器会找到最小那颗树遍历。</span></div>\n<div><span>&nbsp;</span></div>\n<div><span>不同 count()的用法（InnoDB）</span></div>\n<ul>\n<li>\n<div><span>count(*) mysql做了优化</span></div>\n</li>\n<li>\n<div><span>count(1) 遍历整张表，不取值，对于返回的每一行，放一个数字&ldquo;1&rdquo;进去，判断到不可能为空，按行累加。</span></div>\n</li>\n<li>\n<div><span>count(字段) 如果该字段 设置为notNull ，一行行的从记录里面取出该字段，判断不能为Null，按行累加。</span></div>\n</li>\n<li>\n<div><span>count(字段) 如果该字段 设置为允许Null，一行行从记录取该字段，发现可能为Null，再次判断是否为Null，不是Null累加。</span></div>\n</li>\n</ul>\n<div><span>结论：效率排序&nbsp;count(*)&asymp;count(1)&gt;count(主键 id)&gt;count(字段)</span></div>\n<div><span>&nbsp;</span></div>\n<div>InnoDB&nbsp;count(*)解决方案</div>\n<ol>\n<li>\n<div>计数存入Redis中，缺点不能完全保证数据一致性，会有数据丢失风险。</div>\n</li>\n<li>\n<div>计数存入MySQL，利用事务保证数据一致性。</div>\n</li>\n</ol>', 'count(*) 这么慢，我该怎么办？', '', 0, 1, 1, 519, 4, '2020-08-25 18:30:46', '2020-08-25 18:30:46');
INSERT INTO `t_blog` VALUES (100, 'Redis 数据类型介绍', '<h2>Redis 数据库介绍</h2>\n<p><br />Redis 中，键的数据类型是字符串，但是值的数据类型有很多，常用的数据类型是：字符串、列表、字典、集合、有序集合</p>\n<h3>列表（list）</h3>\n<p>1，列表这种数据类型支持存储一组数据<br />2，两种实现方法：（1）压缩列表（ziplist）（2）双向循环链表<br />* 当列表中存储的数据量比较小时，可以采用压缩列表的方式实现。<br />* 具体需要同时满足下面两个条件：<br />（1）列表中保存的单个数据（可能是字符串类型的）小于 64 字节；<br />（2）列表中数据个数少于 512 个<br /><br />3，关于压缩列表<br />* 它并不是基础数据结构，而是 Redis 自己设计的一种数据存储结构<br />* 类似数组，通过一片连续的内存空间来存储数据<br />* 跟数组不同的是它允许存储的数据大小不同<br /><br />4，压缩列表中的&ldquo;压缩&rdquo;如何理解？<br />* &ldquo;压缩&rdquo;：就是节省内存，之所以说节省内存，是相较于数组的存储思路而言的。数组要求每个元素的大小相同，如果要存储不同长度的字符串，就需要用最大长度的字符串大小作为元素的大小。但压缩数组允许不同的存储空间。<br /><br />* 压缩列表这种存储结构，另一方面可以支持不同类型数据的存储<br />* 数据存储在一片连续的内存空间，通过键来获取值为列表类型的数据，读取的效率也非常高。<br /><br />5，不能同时满足压缩列表的两个条件时，列表就要通过双向循环链表来实现</p>\n<h3>字典（hash）</h3>\n<p>1，字典类型用来存储一组数据对。<br />2，每个数据对又包含键值两部分，也有两种实现方式：（1）压缩列表（2）散列表<br />3，同样，只有当存储的数据量比较小的情况下，Redis 才使用压缩列表来实现字典类型。具体需要满足两个条件：<br /><br />（1）字典中保存的键和值的大小都要小于 64 字节<br />（2）字典中键值对的个数要小于 512 个<br /><br />4，当不能同时满足上面两个条件的时候，Redis 就使用散列表来实现字典类型<br />* Redis 使用MurmurHash2这种运行速度快、随机性好的哈希算法作为哈希函数<br />* 对于哈希冲突问题，Redis 使用链表法来解决<br />* 除此之外，Redis 还支持散列表的动态扩容、缩容。<br /><br />当数据动态增加，装载因子会不停地变大。为了避免散列表性能的下降，当装载因子大于 1 的时候，Redis 会触发扩容，将散列表扩大为原来大小的 2 倍左右（具体值需要计算才能得到）。<br /><br />当数据动态减少之后，为了节省内存，当装载因子小于 0.1 的时候，Redis 就会触发缩容，缩小为字典中数据个数的大约 2 倍大小（这个值也是计算得到的）<br /><br />扩容缩容要做大量的数据搬移和哈希值的重新计算，比较耗时。针对这个问题，Redis 使用渐进式扩容缩容策略：将数据的搬移分批进行，避免了大量数据一次性搬移导致的服务停顿。</p>\n<h3>集合（set）</h3>\n<p>1，集合这种数据类型用来存储一组不重复的数据<br />2，这种数据类型也有两种实现方法：（1）有序数组（2）散列表<br />3，Redis 若采用有序数组，要同时满足下面这样两个条件：<br /><br />（1）存储的数据都是整数；<br />（2）存储的数据元素个数不超过 512 个。<br />当不能同时满足这两个条件的时候，Redis 就使用散列表来存储集合中的数据。</p>\n<h3>有序集合（sortedset</h3>\n<p>1，它用来存储一组数据，并且每个数据会附带一个得分。通过得分的大小，将数据组织成跳表这样的数据结构，以支持快速地按照得分值、得分区间获取数据。<br />2，当数据量比较小的时候，Redis 可用压缩列表来实现有序集合。使用的前提有两个：<br /><br />（1）所有数据的大小都要小于 64 字节；<br />（2）元素个数要小于 128 个<br /><br /></p>', 'Redis 数据类型介绍', '', 0, 1, 1, 534, 7, '2020-08-26 15:33:57', '2020-08-26 15:33:57');
INSERT INTO `t_blog` VALUES (102, 't1', '<p>&lt;img class=\"wscnph\" src=\"http://sjpeng.top/aefe91c646844997a90e7a16f99f46bc.jpg\"&gt;</p>\n<p>&lt;svg/onload=alert`xss`&gt;</p>', '01', '', 0, 1, 0, 4, 1, '2020-09-19 13:35:06', '2020-09-19 13:35:06');
INSERT INTO `t_blog` VALUES (103, 'ZooKeeper分布式锁的实现原理', '<p><a href=\"https://www.cnblogs.com/ysw-go/p/11444993.html\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>', 'ZooKeeper分布式锁的实现原理', '', 0, 1, 1, 477, 7, '2020-10-14 18:16:31', '2020-10-14 18:16:31');
INSERT INTO `t_blog` VALUES (104, 'scrapy爬取王者荣耀英雄图片', '<p>一 .需要准备的工具</p>\n<p>Pycharm、Python3.7、scrapy框架</p>\n<p>二 . 预期目标</p>\n<p data-tool=\"mdnice编辑器\">创建一个文件夹， 里面又有按英雄名称分的子文件夹保存该英雄的所有皮肤图片。</p>\n<p data-tool=\"mdnice编辑器\">URL：https://pvp.qq.com/web201605/herolist.shtml</p>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/80892508d4904c86b5b2ad8c3160a7c1.png\" /></p>\n<p>三 .编写程序</p>\n<ol>\n<li>首先创建scrapy项目，控制台输入 <code>scrapy startproject <span style=\"color: #fa7a00;\">mySpider </span></code>创建项目</li>\n<li>创建爬虫爬虫名字可以叫zeroImages 命令如下：scrapy genspider zeroImages \"qq.com\"</li>\n<li>选择要爬取的首页，F12分析代码 <a href=\"https://pvp.qq.com/web201605/herolist.shtml\" target=\"_blank\" rel=\"noopener\">https://pvp.qq.com/web201605/herolist.shtml</a></li>\n<li>分析网页，进入首页随意点一个英雄查看详情<img class=\"wscnph\" src=\"http://sjpeng.top/471a2b68c7664e6da12f7b7d83b2673a.gif\" />多选择几个英雄检查网页，可以发现各个英雄页面的 URL 规律\n<pre class=\"language-markup\"><code>https://pvp.qq.com/web201605/herodetail/152.shtml\nhttps://pvp.qq.com/web201605/herodetail/150.shtml\nhttps://pvp.qq.com/web201605/herodetail/167.shtml</code></pre>\n<p>&nbsp;</p>\n<p data-tool=\"mdnice编辑器\">发现只有末尾的数字在变化，末尾的数字可以认为是该英雄的页面标识。</p>\n<p data-tool=\"mdnice编辑器\">点击 Network，Crtl + R 刷新，可以找到一个 herolist.json 文件。<img class=\"wscnph\" src=\"http://sjpeng.top/9b8921e603ce492a99006ff483dac1e7.gif\" /></p>\n双击这个 json 文件，将它下载下来观察，用编辑器打开可以看到。<img class=\"wscnph\" src=\"http://sjpeng.top/69c4bcd7a5a34b5fb7982947253fad51.jpg\" />\n<p data-tool=\"mdnice编辑器\">ename 是英雄网址页面的标识；而 cname 是对应英雄的名称；skin_name 为对应皮肤的名称。</p>\n<p data-tool=\"mdnice编辑器\">任选一个英雄页面进去，检查该英雄下面所有皮肤，观察 url 变化规律。<img class=\"wscnph\" src=\"http://sjpeng.top/14d7e57db9d14ba3b1467aba81dbc801.gif\" />url变化规律如下：</p>\n<pre class=\"language-markup\"><code>https://game.gtimg.cn/images/yxzj/img201606/heroimg/152/152-bigskin-1.jpghttps://game.gtimg.cn/images/yxzj/img201606/heroimg/152/152-bigskin-2.jpghttps://game.gtimg.cn/images/yxzj/img201606/heroimg/152/152-bigskin-3.jpghttps://game.gtimg.cn/images/yxzj/img201606/heroimg/152/152-bigskin-4.jpghttps://game.gtimg.cn/images/yxzj/img201606/heroimg/152/152-bigskin-5.jpg</code></pre>\n<p>观察到同一个英雄的皮肤图片 url 末尾 <code>-{x}.jpg</code> 从 <code>1</code> 开始依次递增，再来看看不同英雄的皮肤图片 url 是如何构造的。会发现， ename 这个英雄的标识不一样，获取到的图片就不一样，由 ename 参数决定。</p>\n<pre class=\"language-markup\"><code>https://game.gtimg.cn/images/yxzj/img201606/heroimg/152/152-bigskin-1.jpg\nhttps://game.gtimg.cn/images/yxzj/img201606/heroimg/150/150-bigskin-1.jpg\nhttps://game.gtimg.cn/images/yxzj/img201606/heroimg/153/153-bigskin-1.jpg\n# 可构造图片请求链接如下\nhttps://game.gtimg.cn/images/yxzj/img201606/heroimg/{ename}/{ename}-bigskin-{x}.jpg​</code></pre>\n</li>\n<li>代码实现，数据提取。\n<pre class=\"language-python\"><code>import scrapy\nimport json\nimport re\nfrom wzry.items import HeroItem\n\n\nclass HeroimagesSpider(scrapy.Spider):\n    name = \'heroImages\'\n    allowed_domains = [\'qq.com\']\n    start_urls = [\'https://pvp.qq.com/web201605/js/herolist.json\']\n\n    # 根据Json文件构造url地址\n    # 可构造图片请求链接如下\n    # https://game.gtimg.cn/images/yxzj/img201606/heroimg/{ename}/{ename}-bigskin-{x}.jpg\n    def parse(self, response):\n        tar = \"https://game.gtimg.cn/images/yxzj/img201606/heroimg/{ename}/{ename}-bigskin-{x}.jpg\"\n        data_list = json.loads(response.text)\n        print(data_list)\n        for data in data_list:\n            cname = data[\"cname\"]\n            ename = data[\"ename\"]\n            skin_list_name = data[\"skin_name\"].split(\'|\')\n            skin_list_name.insert(0, \"默认\")\n            for i in range(len(skin_list_name)):\n                item = HeroItem()\n                item[\"name\"] = cname +\"-\"+ skin_list_name[i]\n                tem = re.sub(\"{ename}\", str(ename), tar)\n                item[\"image_url\"] = re.sub(\"{x}\", str(i + 1), tem)\n                yield item​</code></pre>\n<p>&nbsp;</p>\n</li>\n<li>修改settings文件，开启pipelines管道, 配置用户代理，设置存放文件的路径。\n<pre class=\"language-python\"><code>import random\nimport os\n\nBOT_NAME = \'wzry\'\n\nSPIDER_MODULES = [\'wzry.spiders\']\nNEWSPIDER_MODULE = \'wzry.spiders\'\n\nLOG_LEVEL = \"WARNING\"\n\n#开启管道\nITEM_PIPELINES = {\n   \'wzry.pipelines.WzryPipeline\': 300,\n}\n\n# 设置图片下载后的存储路径，放到工程目录下images文件夹\n# 获取当前目录绝对路径\nproject_dir = os.path.abspath(os.path.dirname(__file__))\n# images存储路径\nIMAGES_STORE = os.path.join(project_dir,\'images/\')\n\n# Crawl responsibly by identifying yourself (and your website) on the user-agent\n#USER_AGENT = \'wzry (+http://www.yourdomain.com)\'\n\n#代理列表\nUSER_AGENT_LIST = [\n    \'MSIE (MSIE 6.0; X11; Linux; i686) Opera 7.23\',\n    \'Opera/9.20 (Macintosh; Intel Mac OS X; U; en)\',\n    \'Opera/9.0 (Macintosh; PPC Mac OS X; U; en)\',\n    \'iTunes/9.0.3 (Macintosh; U; Intel Mac OS X 10_6_2; en-ca)\',\n    \'Mozilla/4.76 [en_jp] (X11; U; SunOS 5.8 sun4u)\',\n    \'iTunes/4.2 (Macintosh; U; PPC Mac OS X 10.2)\',\n    \'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:5.0) Gecko/20100101 Firefox/5.0\',\n    \'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0) Gecko/20100101 Firefox/9.0\',\n    \'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20120813 Firefox/16.0\',\n    \'Mozilla/4.77 [en] (X11; I; IRIX;64 6.5 IP30)\',\n    \'Mozilla/4.8 [en] (X11; U; SunOS; 5.7 sun4u)\'\n]\n# 随机生成user agent\n\nUSER_AGENT = random.choice(USER_AGENT_LIST)​</code></pre>\n</li>\n<li>编写管道继承 ImagePipeline类，重写get_media_requests和item_completed方法\n<p>&nbsp;</p>\n<pre class=\"language-python\"><code>import os\nimport scrapy\nimport wzry.items as Items\nfrom scrapy.pipelines.images import ImagesPipeline\nfrom wzry.settings import IMAGES_STORE as images_store\n\n\nclass WzryPipeline(ImagesPipeline):\n    # 配置下载图片的url\n    def get_media_requests(self, item, info):\n        print(item)\n        yield scrapy.Request(item[\'image_url\'])\n\n    # 设置下载到本地的文件name\n    def item_completed(self, results, item, info):\n        image_path = [x[\"path\"] for ok, x in results if ok]\n        os.rename(images_store + image_path[0], images_store + item[\"name\"] + \".jpg\")\n</code></pre>\n</li>\n<li>控制台执行 启动爬虫命令 scrapy crawl zeroImages&nbsp;</li>\n</ol>\n<p>代码已上传github:<a href=\"https://github.com/lurenha/WangzheImages\" target=\"_blank\" rel=\"noopener\">https://github.com/lurenha/WangzheImages</a></p>\n<p>参考：<a href=\"https://mp.weixin.qq.com/s?__biz=MzkwNjEzMTI4NA==&amp;mid=2247486875&amp;idx=1&amp;sn=ed2147a6503d416d13750b050569fc00&amp;chksm=c0ec6fa0f79be6b68578be63427e6bec94b4b9240821c4e0805b81fc76537a1efed314657b75&amp;scene=126&amp;sessionid=1603246015&amp;key=50c6d0ff50751a99a200245a078df9504a095788af812d3d0d7775de449e817bbddcffe556d2179c0626257eea7a793bb7097e6a0ab3e7cf8e5169d55bd74e5a2d7c3c629a26312f661e0ae5579bf15dcae1e7a6dafa1fb3ebacf45801a568c5212c53d6cd11a89c518dc9ba44fa3d450313644d07bf86123718faae458b03c6&amp;ascene=1&amp;uin=MjYxNzg2NTUwNg%3D%3D&amp;devicetype=Windows+10+x64&amp;version=6300002f&amp;lang=zh_CN&amp;exportkey=A1kbcU9Qp46%2BgA%2FN6kvXZ7w%3D&amp;pass_ticket=zm7f%2FRyQ%2BNfo3dXOZgq%2Bo9%2FpQ%2FpVMZdy60oLVtmJhcfNimku3LSbT3nNJUa%2FOdPn&amp;wx_header=0\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>', 'scrapy爬取王者荣耀英雄图片', '', 0, 1, 1, 685, 9, '2020-10-20 14:31:06', '2020-10-20 14:31:06');
INSERT INTO `t_blog` VALUES (105, '安利一个简历制作神器！', '<p>给大家安利一个写简历的网站，可以选择多个模板，支持导出pdf。</p>\n<p><a href=\"https://www.qmjianli.com/\" target=\"_blank\" rel=\"noopener\">全民简历</a></p>\n<p>需要将写好的简历网址粘贴到如下地址即可导出。</p>\n<p><a href=\"http://lurenpeng.cn:8066\" target=\"_blank\" rel=\"noopener\">导出简历</a></p>\n<p>注意:需要公开访问权限，（进入个人中心-&gt;选择简历-&gt;公开访问）</p>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/7a5f27edc7414e7c95233231ddd8eef7.png\" /><img class=\"wscnph\" src=\"http://sjpeng.top/117ab5992fb245ada87142333188b7e0.png\" /><img class=\"wscnph\" src=\"http://sjpeng.top/01c2d85bb7ce4b74b669ec583833c49a.png\" /><img class=\"wscnph\" src=\"http://sjpeng.top/3341dcc1298f428aab328d3fcc947ba5.png\" /><img class=\"wscnph\" src=\"http://sjpeng.top/73d861d8eb9f492480558caf60612403.png\" /></p>\n<p>&nbsp;</p>', '安利一个制作简历的网站', '', 0, 1, 1, 759, 8, '2021-01-07 19:06:43', '2021-01-07 19:08:45');
INSERT INTO `t_blog` VALUES (106, 'IP地址 int->String 互转', '<pre class=\"language-java\"><code>/**\n * @author: weipeng\n * @date: 2021/8/9\n */\npublic class IpConvert {\n    private static int str2Int(String ip) {\n        int res = 0;\n        if (ip == null || ip.length() == 0) {\n            return res;\n        }\n        String[] split = ip.split(\"\\\\.\");\n        for (int i = 0; i &lt; split.length; i++) {\n            //第一次是最高8位\n            String tem = split[i];\n            //左移8位后 做或运算，将8位tem放到末尾\n            res = (res &lt;&lt; 8) | Integer.valueOf(tem);\n        }\n        return res;\n    }\n\n    private static String int2Str(int ip) {\n        String[] tarStr = new String[4];\n        for (int i = 0; i &lt; tarStr.length; i++) {\n            //取最右边8位后 右移8位后 方便下次取数\n            int tem = ip &amp; 255;\n            //第一次是最右边 依次往左放\n            tarStr[3 - i] = String.valueOf(tem);\n            ip = ip &gt;&gt; 8;\n        }\n        return String.join(\".\", tarStr);\n    }\n\n    public static void main(String[] args) {\n        String res = int2Str(str2Int(\"192.168.31.1\"));\n        System.out.println(res);\n    }\n}</code></pre>', 'IP地址刚好可以用一个32位int表示 节省空间', '', 0, 1, 1, 156, 3, '2021-08-09 19:04:26', '2021-08-09 19:04:26');
INSERT INTO `t_blog` VALUES (107, '40. 组合总和 II', '<p>给定一个数组&nbsp;candidates&nbsp;和一个目标数&nbsp;target&nbsp;，找出&nbsp;candidates&nbsp;中所有可以使数字和为&nbsp;target&nbsp;的组合。</p>\n<p>candidates&nbsp;中的每个数字在每个组合中只能使用一次。</p>\n<p>注意：解集不能包含重复的组合。&nbsp;</p>\n<p>&nbsp;</p>\n<p>示例&nbsp;1:</p>\n<p>输入: candidates =&nbsp;[10,1,2,7,6,1,5], target =&nbsp;8,<br />输出:<br />[<br />[1,1,6],<br />[1,2,5],<br />[1,7],<br />[2,6]<br />]</p>\n<p><br />示例&nbsp;2:</p>\n<p>输入: candidates =&nbsp;[2,5,2,1,2], target =&nbsp;5,<br />输出:<br />[<br />[1,2,2],<br />[5]<br />]</p>\n<hr />\n<p>关键在于如何去重，</p>\n<p>排序两点用处：1.方便剪枝（大的在后面，小的放不进去，后面的也无需判断）2.去重</p>\n<p>重点看&ldquo;去重&rdquo;的逻辑：在一个递归树里 待选数组中 相同数字跳过（已排序相同的一定相邻）</p>\n<p>参考图中 红色分支都是重复的,每个递归流程中 待选项中如果有重复 只需选取首次分支即可</p>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/a123bc3ff9514739a796ecd0796dc12a.png\" /></p>\n<hr />\n<h2>Python:</h2>\n<pre class=\"language-python\"><code>class Solution:\n    def combinationSum2(self, candidates: List[int], target: int) -&gt; List[List[int]]:\n        res = []\n        tem = []\n        # 排序为了后续剪枝和去重\n        candidates.sort()\n\n        def backStack(nextIndex, nextTar):\n            if nextTar == 0:\n                res.append(tem[:])\n                return\n            for i in range(nextIndex, len(candidates)):\n                # 剪枝，避免多余判断\n                if nextTar &lt; candidates[i]:\n                    break\n                # 去重\n                if i &gt; nextIndex and candidates[i] == candidates[i - 1]:\n                    continue\n                tem.append(candidates[i])\n                backStack(i + 1, nextTar - candidates[i])\n                tem.pop()\n\n        backStack(0, target)\n        return res</code></pre>\n<h2>Java:</h2>\n<pre class=\"language-java\"><code>List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();\n    List&lt;Integer&gt; tem = new ArrayList&lt;&gt;();\n    public List&lt;List&lt;Integer&gt;&gt; combinationSum2(int[] candidates, int target) {\n        Arrays.sort(candidates);\n        backStack(candidates, target, 0);\n        return res;\n    }\n\n    private void backStack(int[] candidates, int temTarget, int i) {\n        if (temTarget == 0) {\n            res.add(new ArrayList&lt;&gt;(tem));\n            return;\n        }\n        for (int j = i; j &lt; candidates.length; j++) {\n            if (temTarget - candidates[j] &lt; 0) {\n                break;\n            }\n            if (j &gt; i &amp;&amp; candidates[j - 1] == candidates[j]) {\n                continue;\n            }\n            tem.add(candidates[j]);\n            backStack(candidates, temTarget - candidates[j], j+1);\n            tem.remove(tem.size() - 1);\n        }\n    }</code></pre>', '回溯+剪枝', '', 0, 1, 1, 125, 2, '2021-08-11 23:42:33', '2021-08-11 23:42:33');
INSERT INTO `t_blog` VALUES (108, '1143.最长公共子序列 718.最长重复子数组', '<pre><strong>最长公共子串：</strong><br /><br /># 给定两个字符串 text1 和 text2，返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ，返回 0 。 <br /># <br />#  一个字符串的 子序列 是指这样一个新的字符串：它是由原字符串在不改变字符的相对顺序的情况下删除某些字符（也可以不删除任何字符）后组成的新字符串。 <br /># <br />#  <br />#  例如，\"ace\" 是 \"abcde\" 的子序列，但 \"aec\" 不是 \"abcde\" 的子序列。 <br />#  <br /># <br />#  两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。 <br /># <br />#  <br /># <br />#  示例 1： <br /># <br />#  <br /># 输入：text1 = \"abcde\", text2 = \"ace\" <br /># 输出：3  <br /># 解释：最长公共子序列是 \"ace\" ，它的长度为 3 。<br />#  <br /># <br />#  示例 2： <br /># <br />#  <br /># 输入：text1 = \"abc\", text2 = \"abc\"<br /># 输出：3<br /># 解释：最长公共子序列是 \"abc\" ，它的长度为 3 。<br />#  <br /># <br />#  示例 3： <br /># <br />#  <br /># 输入：text1 = \"abc\", text2 = \"def\"<br /># 输出：0<br /># 解释：两个字符串没有公共子序列，返回 0 。<br />#  <br /># <br />#  <br /># <br />#  提示： <br /># <br />#  <br />#  1 &lt;= text1.length, text2.length &lt;= 1000 <br />#  text1 和 text2 仅由小写英文字符组成。 <br />#  <br />#  Related Topics 字符串 动态规划 <br /><br /><br /><br /><strong>最长公共子序列：<br /></strong># 给两个整数数组 A 和 B ，返回两个数组中公共的、长度最长的子数组的长度。 <br /># <br />#  <br /># <br />#  示例： <br /># <br />#  输入：<br /># A: [1,2,3,2,1]<br /># B: [3,2,1,4,7]<br /># 输出：3<br /># 解释：<br /># 长度最长的公共子数组是 [3, 2, 1] 。<br />#  <br />#<br /><br />#  <br /># <br />#  提示： <br /># <br />#  <br />#  1 &lt;= len(A), len(B) &lt;= 1000 <br />#  0 &lt;= A[i], B[i] &lt; 100 <br />#  <br />#  Related Topics 数组 二分查找 动态规划 滑动窗口 哈希函数 滚动哈希 </pre>\n<hr />\n<pre>区别：子序列可以是不连续的，子串必须连续<br />比如：&ldquo;abcd&rdquo;和&ldquo;acd&rdquo;最长公共子串是&ldquo;cd&rdquo;，最长公共子序列是&ldquo;acd&rdquo;</pre>\n<div class=\"math display\">\n<div class=\"MathJax_Display\">子串：\n<pre class=\"language-python\"><code>class Solution:\n    def findLength(self, nums1: List[int], nums2: List[int]) -&gt; int:\n        #dp[i][j]代表：nums1[i]为起点倒排 与 nums2[j]为起点倒排 的公共部分长度\n        dp = [[0] * len(nums2) for _ in range(0, len(nums1))]\n        res = 0\n        #dp初始化\n        if nums2[0] == nums1[0]:\n            dp[0][0] = 1\n            res = max(dp[0][0], res)\n        for i in range(1, len(nums1)):\n            if nums2[0] == nums1[i]:\n                dp[i][0] = 1\n                res = max(dp[i][0], res)\n        for j in range(1, len(nums2)):\n            if nums2[j] == nums1[0]:\n                dp[0][j] = 1\n                res = max(dp[0][j], res)\n        #dp状态转移\n        for i in range(1, len(nums1)):\n            for j in range(1, len(nums2)):\n                if nums1[i] == nums2[j]:\n                    dp[i][j] = dp[i - 1][j - 1] + 1\n                    res = max(res, dp[i][j])\n        return res​</code></pre>\n</div>\n<div class=\"MathJax_Display\">&nbsp;</div>\n</div>\n<p>子序列：</p>\n<pre class=\"language-python\"><code>class Solution:\n    def longestCommonSubsequence(self, text1: str, text2: str) -&gt; int:\n        #dp[i][j]代表 text1[0-i] 与 text2[0-j] 的最长公共子序列\n        dp = [[0] * len(text2) for _ in range(0, len(text1))]\n        #dp初始化\n        if text2[0] == text1[0]:\n            dp[0][0] = 1\n        for i in range(1, len(text1)):\n            if text2[0] == text1[i]:\n                dp[i][0] = 1\n            else:\n                dp[i][0] = dp[i - 1][0]\n        for j in range(1, len(text2)):\n            if text2[j] == text1[0]:\n                dp[0][j] = 1\n            else:\n                dp[0][j] = dp[0][j - 1]\n        #dp状态转移\n        for i in range(1, len(text1)):\n            for j in range(1, len(text2)):\n                dp[i][j] = dp[i - 1][j - 1]\n                if text1[i] == text2[j]:\n                    dp[i][j] += 1\n                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], dp[i][j])\n\n        return dp[-1][-1]</code></pre>', '最长公共子串和子序列', '', 0, 1, 1, 139, 2, '2021-08-12 16:35:19', '2021-08-12 16:35:19');
INSERT INTO `t_blog` VALUES (109, '3. 无重复字符的最长子串', '<pre># 给定一个字符串 s ，请你找出其中不含有重复字符的 最长子串 的长度。 <br /># <br />#  <br /># <br />#  示例 1: <br /># <br />#  <br /># 输入: s = \"abcabcbb\"<br /># 输出: 3 <br /># 解释: 因为无重复字符的最长子串是 \"abc\"，所以其长度为 3。<br />#  <br /># <br />#  示例 2: <br /># <br />#  <br /># 输入: s = \"bbbbb\"<br /># 输出: 1<br /># 解释: 因为无重复字符的最长子串是 \"b\"，所以其长度为 1。<br />#  <br /># <br />#  示例 3: <br /># <br />#  <br /># 输入: s = \"pwwkew\"<br /># 输出: 3<br /># 解释: 因为无重复字符的最长子串是&nbsp;\"wke\"，所以其长度为 3。<br /># &nbsp;    请注意，你的答案必须是 子串 的长度，\"pwke\"&nbsp;是一个子序列，不是子串。<br />#  <br /># <br />#  示例 4: <br /># <br />#  <br /># 输入: s = \"\"<br /># 输出: 0<br />#  <br /># <br />#  <br /># <br />#  提示： <br /># <br />#  <br />#  0 &lt;= s.length &lt;= 5 * 104 <br />#  s 由英文字母、数字、符号和空格组成 <br />#  <br />#  Related Topics 哈希表 字符串 滑动窗口 <br /><br />典型的滑动窗口<br />思路：窗口向右滑动，同时记录两个值：1.Left Right间最长距离ret 2.Left Right间元素的坐标（可以用Map记录）<br /> &nbsp;   向右滑动后两种情况  1.当前Right对应元素 是Left Right间的元素（Map中） Left滑动到 Map记录元素坐标+1 位置，同时清楚Map中无效的元素（不在Left Right间元素）<br />&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;  2.当前Right对应元素 不在Map中，放入Map，继续往右滑<br /><br /></pre>\n<pre class=\"language-python\"><code>class Solution:\n    def lengthOfLongestSubstring(self, s: str) -&gt; int:\n        map = {}\n        # i:left j:right\n        ret = 0\n        i = 0\n        for j in range(0, len(s)):\n            # 向右化后 发现在Map中\n            if s[j] in map.keys():\n                # 这个max 为了过滤一种情况：上次i更新后 map中 old i-j间的元素 就无效了，省去对Map delete操作\n                # 也可以理解为 i和j 都只能向右滑动\n                i = max(map[s[j]] + 1, i)\n            map[s[j]] = j\n            # 随时记录i,j间距离\n            ret = max(j - i + 1, ret)\n        return ret</code></pre>', '给定一个字符串 s ，请你找出其中不含有重复字符的 最长子串 的长度。', '', 0, 1, 1, 128, 2, '2021-08-12 21:53:32', '2021-08-12 22:02:07');
INSERT INTO `t_blog` VALUES (110, '112.路径总和 113.路径总和2', '<h2>112.路径总和1</h2>\n<pre># 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ，判断该树中是否存在 根节点到叶子节点 的路径，这条路径上所有节点值相加等于目标和<br />#  targetSum 。 <br /># <br />#  叶子节点 是指没有子节点的节点。 <br /># <br />#  <br /># <br />#  示例 1： <br /># <br />#  <br /># 输入：root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22<br /># 输出：true<br />#  <br /># <br />#  示例 2： <br /># <br />#  <br /># 输入：root = [1,2,3], targetSum = 5<br /># 输出：false<br />#  <br /># <br />#  示例 3： <br /># <br />#  <br /># 输入：root = [1,2], targetSum = 0<br /># 输出：false<br />#  <br /># <br />#  <br /># <br />#  提示： <br /># <br />#  <br />#  树中节点的数目在范围 [0, 5000] 内 <br />#  -1000 &lt;= Node.val &lt;= 1000 <br />#  -1000 &lt;= targetSum &lt;= 1000 <br />#  <br />#  Related Topics 树 深度优先搜索 二叉树 </pre>\n<pre class=\"language-python\"><code>class Solution:\n    def hasPathSum(self, root: TreeNode, targetSum: int) -&gt; bool:\n        def backStack(tem: TreeNode, target):\n            if not tem:\n                return False\n            # 叶子节点匹配返回True\n            if not tem.left and not tem.right:\n                if tem.val == target:\n                    return True\n            return backStack(tem.left, target - tem.val) or backStack(tem.right, target - tem.val)\n\n        return backStack(root, targetSum)</code></pre>\n<h2>113.路径总和2</h2>\n<pre># 给你二叉树的根节点 root 和一个整数目标和 targetSum ，找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 <br /># <br />#  叶子节点 是指没有子节点的节点。 <br /># <br />#  <br />#  <br />#  <br /># <br />#  示例 1： <br /># <br />#  <br /># 输入：root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22<br /># 输出：[[5,4,11,2],[5,8,4,5]]<br />#  <br /># <br />#  示例 2： <br /># <br />#  <br /># 输入：root = [1,2,3], targetSum = 5<br /># 输出：[]<br />#  <br /># <br />#  示例 3： <br /># <br />#  <br /># 输入：root = [1,2], targetSum = 0<br /># 输出：[]<br />#  <br /># <br />#  <br /># <br />#  提示： <br /># <br />#  <br />#  树中节点总数在范围 [0, 5000] 内 <br />#  -1000 &lt;= Node.val &lt;= 1000 <br />#  -1000 &lt;= targetSum &lt;= 1000 <br />#  <br />#  <br />#  <br />#  Related Topics 树 深度优先搜索 回溯 二叉树 </pre>\n<pre class=\"language-python\"><code>class Solution:\n    def pathSum(self, root: Optional[TreeNode], targetSum: int) -&gt; List[List[int]]:\n        ret = []\n        tem = []\n\n        def backStack(root1: Optional[TreeNode], tar):\n            if not root1:\n                return\n            # 找到叶子节点\n            if not root1.left and not root1.right:\n                if root1.val == tar:\n                    tem.append(root1.val)\n                    ret.append(tem[:])\n                    tem.pop()\n                else:\n                    return\n            # 回溯\n            tem.append(root1.val)\n            backStack(root1.left, tar - root1.val)\n            backStack(root1.right, tar - root1.val)\n            tem.pop()\n\n        backStack(root, targetSum)\n        return ret</code></pre>', '112.路径总和 113.路径总和2', '', 0, 1, 1, 157, 2, '2021-08-15 10:48:22', '2021-08-15 10:48:22');
INSERT INTO `t_blog` VALUES (111, '剑指 Offer 36. 二叉搜索树与双向链表', '<p>输入一棵二叉搜索树，将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点，只能调整树中节点指针的指向。</p>\n<p>&nbsp;</p>\n<p>为了让您更好地理解问题，以下面的二叉搜索树为例：</p>\n<p><img src=\"https://assets.leetcode.com/uploads/2018/10/12/bstdlloriginalbst.png\" /></p>\n<p>我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表，第一个节点的前驱是最后一个节点，最后一个节点的后继是第一个节点。</p>\n<p>下图展示了上面的二叉搜索树转化成的链表。&ldquo;head&rdquo; 表示指向链表中有最小元素的节点。</p>\n<p><img src=\"https://assets.leetcode.com/uploads/2018/10/12/bstdllreturndll.png\" /></p>\n<p>特别地，我们希望可以就地完成转换操作。当转化完成以后，树中节点的左指针需要指向前驱，树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。</p>\n<p>利用二叉搜索树的中序遍历 即为排序后的值，按顺序生成链表即可</p>\n<p>头节点：二叉搜索树中序遍历第一个节点 是最小元素即头节点</p>\n<p>最后需要头尾互联，形成循环链表</p>\n<pre class=\"language-python\"><code>class Solution:\n\n    def treeToDoublyList(self, root: \'Node\') -&gt; \'Node\':\n        self.head = None\n        self.pre = None\n\n        def dfs(cur):\n            if not cur:\n                return\n            dfs(cur.left)\n            # 中序第一次访问到的节点为头节点\n            if not self.head:\n                self.head = cur\n            if self.pre:\n                cur.left = self.pre\n                self.pre.right = cur\n                self.pre = self.pre.right\n            else:\n                self.pre = cur\n            dfs(cur.right)\n\n        if not root:\n            return None\n        dfs(root)\n        # 头尾互联构成循环链表\n        self.head.left = self.pre\n        self.pre.right = self.head\n        return self.head</code></pre>', '输入一棵二叉搜索树，将该二叉搜索树转换成一个排序的循环双向链表。', '', 0, 1, 1, 155, 2, '2021-08-18 22:44:15', '2021-08-18 22:44:15');
INSERT INTO `t_blog` VALUES (112, '剑指 Offer 38. 字符串的排列', '<p>输入一个字符串，打印出该字符串中字符的所有排列。</p>\n<p>&nbsp;</p>\n<p>你可以以任意顺序返回这个字符串数组，但里面不能有重复元素。</p>\n<p><strong>示例:</strong></p>\n<pre><strong>输入：</strong>s = \"abc\"\n<strong>输出：[</strong>\"abc\",\"acb\",\"bac\",\"bca\",\"cab\",\"cba\"<strong>]<br /><br /></strong></pre>\n<p><strong>限制：</strong></p>\n<p><code>1 &lt;= s 的长度 &lt;= 8</code></p>\n<hr />\n<pre><strong>与 <a href=\"http://lurenpeng.cn/peng/blog/69\" target=\"_blank\" rel=\"noopener\">全排列 II</a> 相同<br /></strong></pre>\n<pre class=\"language-python\"><code>class Solution:\n    def permutation(self, s: str) -&gt; List[str]:\n        s = list(s)\n        lens = len(s)\n        ret = []\n\n        def swap(i, j):\n            tem = s[i]\n            s[i] = s[j]\n            s[j] = tem\n\n        def backStack(begin):\n            if begin == lens:\n                ret.append(\"\".join(s))\n                return\n            myset = set()\n            for i in range(begin, lens):\n                if s[i] in myset:\n                    continue\n                else:\n                    myset.add(s[i])\n                swap(i, begin)\n                backStack(begin + 1)\n                swap(i, begin)\n\n        backStack(0)\n        return ret</code></pre>\n<pre><strong>&nbsp;</strong></pre>', '剑指 Offer 38. 字符串的排列', '', 0, 1, 1, 132, 2, '2021-08-19 21:01:30', '2021-08-19 21:01:30');
INSERT INTO `t_blog` VALUES (113, '45. 跳跃游戏 II', '<p>给你一个非负整数数组&nbsp;nums ，你最初位于数组的第一个位置。</p>\n<p>数组中的每个元素代表你在该位置可以跳跃的最大长度。</p>\n<p>你的目标是使用最少的跳跃次数到达数组的最后一个位置。</p>\n<p>假设你总是可以到达数组的最后一个位置。</p>\n<p>&nbsp;</p>\n<p>示例 1:</p>\n<p>输入: nums = [2,3,1,1,4]<br />输出: 2<br />解释: 跳到最后一个位置的最小跳跃数是 2。<br />&nbsp; 从下标为 0 跳到下标为 1 的位置，跳&nbsp;1&nbsp;步，然后跳&nbsp;3&nbsp;步到达数组的最后一个位置。<br />示例 2:</p>\n<p>输入: nums = [2,3,0,1,4]<br />输出: 2<br />&nbsp;</p>\n<p>提示:</p>\n<p>1 &lt;= nums.length &lt;= 104<br />0 &lt;= nums[i] &lt;= 1000<br />通过次数178,064提交次数420,54</p>\n<hr />\n<p>dp[i]代表 当前坐标到终点 跳跃最小次数</p>\n<pre class=\"language-python\"><code>class Solution:\n    def jump(self, nums: List[int]) -&gt; int:\n        dp = [99999] * len(nums)\n        dp[-1] = 0\n        for i in range(len(nums) - 2, -1, -1):\n            cur_jump = nums[i]\n            for j in range(i, i + cur_jump + 1):\n                if j &gt;= len(nums):\n                    break\n                dp[i] = min(dp[i], dp[j] + 1)\n\n        return dp[0]</code></pre>', '45. 跳跃游戏 II', '', 0, 1, 1, 94, 2, '2021-08-21 11:03:58', '2021-08-21 11:03:58');
INSERT INTO `t_blog` VALUES (114, '206. 反转链表', '<p>给你单链表的头节点&nbsp;<code>head</code>&nbsp;，请你反转链表，并返回反转后的链表。</p>\n<p><img src=\"https://assets.leetcode.com/uploads/2021/02/19/rev1ex1.jpg\" /></p>\n<pre><strong>输入：</strong>head = [1,2,3,4,5]\n<strong>输出：</strong>[5,4,3,2,1]</pre>\n<p><img src=\"https://assets.leetcode.com/uploads/2021/02/19/rev1ex2.jpg\" /></p>\n<pre><strong>输入：</strong>head = [1,2]\n<strong>输出：</strong>[2,1]</pre>\n<hr />\n<h2>递归：</h2>\n<pre class=\"language-python\"><code>class Solution:\n    def reverseList(self, head: ListNode) -&gt; ListNode:\n        def backStack(node):\n            if not node or not node.next:\n                return node\n            next = node.next\n            ret=backStack(next)\n            next.next = node\n            node.next = None\n            return ret\n\n        return backStack(head)</code></pre>\n<h2>迭代：</h2>\n<pre class=\"language-python\"><code>class Solution:\n    def reverseList(self, head: ListNode) -&gt; ListNode:\n        cur = head\n        pre = None\n        while cur:\n            next = cur.next\n            cur.next = pre\n            pre = cur\n            cur = next\n        return pre</code></pre>', '206. 反转链表', '', 0, 1, 1, 113, 2, '2021-08-21 11:47:52', '2021-08-21 11:47:52');
INSERT INTO `t_blog` VALUES (115, '5. 最长回文子串', '<p>给你一个字符串 s，找到 s 中最长的回文子串。</p>\n<p>&nbsp;</p>\n<p>示例 1：</p>\n<p>输入：s = \"babad\"<br />输出：\"bab\"<br />解释：\"aba\" 同样是符合题意的答案。<br />示例 2：</p>\n<p>输入：s = \"cbbd\"<br />输出：\"bb\"<br />示例 3：</p>\n<p>输入：s = \"a\"<br />输出：\"a\"<br />示例 4：</p>\n<p>输入：s = \"ac\"<br />输出：\"a\"<br />&nbsp;</p>\n<p>提示：</p>\n<p>1 &lt;= s.length &lt;= 1000<br />s 仅由数字和英文字母（大写和/或小写）组成</p>\n<hr />\n<p>&nbsp;dp[i][j] 代表下标i-j是否为回文串 ,特殊情况1. i==j dp[i][j]是同一个字母 2.i,j相邻 那么他们对应的值相等为回文串</p>\n<p>&nbsp;</p>\n<pre class=\"language-python\"><code>class Solution:\n    def longestPalindrome(self, s: str) -&gt; str:\n        max_len = 0\n        begin = 0\n        dp = [[False] * len(s) for _ in range(len(s))]\n        # 这里i倒叙 因为dp[i][j]依赖dp[i+1][j-1] 需要保证dp[i+1]提前更新\n        for i in range(len(s) - 1, -1, -1):\n            for j in range(i, len(s)):\n                if i == j:\n                    dp[i][j] = True\n                elif s[i] == s[j]:\n                    if i == j - 1:\n                        dp[i][j] = True\n                    elif i + 1 &lt; len(s) and j - 1 &gt; 0 and dp[i + 1][j - 1]:\n                        dp[i][j] = True\n                if dp[i][j]:\n                    if j - i + 1 &gt; max_len:\n                        max_len = j - i + 1\n                        begin = i\n        return s[begin:begin + max_len]</code></pre>', '给你一个字符串 s，找到 s 中最长的回文子串。', '', 0, 1, 1, 91, 2, '2021-08-21 12:44:10', '2021-08-21 12:44:10');
INSERT INTO `t_blog` VALUES (116, '32. 最长有效括号', '<p>给你一个只包含 \'(\'&nbsp;和 \')\'&nbsp;的字符串，找出最长有效（格式正确且连续）括号子串的长度。</p>\n<p>&nbsp;</p>\n<p>示例 1：</p>\n<p>输入：s = \"(()\"<br />输出：2<br />解释：最长有效括号子串是 \"()\"<br />示例 2：</p>\n<p>输入：s = \")()())\"<br />输出：4<br />解释：最长有效括号子串是 \"()()\"<br />示例 3：</p>\n<p>输入：s = \"\"<br />输出：0<br />&nbsp;</p>\n<p>提示：</p>\n<p>0 &lt;= s.length &lt;= 3 * 104<br />s[i] 为 \'(\' 或 \')\'</p>\n<hr />\n<h2>动态规划：</h2>\n<pre class=\"language-python\"><code>class Solution:\n    def longestValidParentheses(self, s: str) -&gt; int:\n        # dp[i] i为终点 的最长回文串长度\n        lens = len(s)\n        if lens == 0:\n            return 0\n        res = 0\n        dp = [0] * lens\n        for i in range(0, lens):\n            # i - dp[i - 1] - 1 代表和 i对应的左括号下标\n            if s[i] == \')\' and i - dp[i - 1] - 1 &gt;= 0 and s[i - dp[i - 1] - 1] == \'(\':\n                if i - dp[i - 1] - 2 &gt;= 0:\n                    dp[i] = 2 + dp[i - 1] + dp[i - dp[i - 1] - 2]\n                else:\n                    dp[i] = 2 + dp[i - 1]\n                res = max(res, dp[i])\n        return res</code></pre>\n<h2>栈：</h2>\n<pre class=\"language-python\"><code>class Solution:\n    def longestValidParentheses(self, s: str) -&gt; int:\n        res = 0\n        # 先放个\')\'\n        stack = [-1]\n        for i in range(len(s)):\n            if s[i] == \'(\':\n                stack.append(i)\n            else:  # s[i]==\')\'\n                stack.pop()\n                if len(stack) == 0:  \n                    stack.append(i)\n                else:\n                    tem = i - stack[-1]\n                    res = max(res, tem)</code></pre>', '32. 最长有效括号', '', 0, 1, 1, 128, 2, '2021-08-23 23:44:11', '2021-08-23 23:44:11');
INSERT INTO `t_blog` VALUES (117, '39. 组合总和', '<p>给定一个无重复元素的正整数数组&nbsp;candidates&nbsp;和一个正整数&nbsp;target&nbsp;，找出&nbsp;candidates&nbsp;中所有可以使数字和为目标数&nbsp;target&nbsp;的唯一组合。</p>\n<p>candidates&nbsp;中的数字可以无限制重复被选取。如果至少一个所选数字数量不同，则两种组合是唯一的。&nbsp;</p>\n<p>对于给定的输入，保证和为&nbsp;target 的唯一组合数少于 150 个。</p>\n<p>示例&nbsp;1：</p>\n<p>输入: candidates = [2,3,6,7], target = 7<br />输出: [[7],[2,2,3]]<br />示例&nbsp;2：</p>\n<p>输入: candidates = [2,3,5], target = 8<br />输出: [[2,2,2,2],[2,3,3],[3,5]]<br />示例 3：</p>\n<p>输入: candidates = [2], target = 1<br />输出: []<br />示例 4：</p>\n<p>输入: candidates = [1], target = 1<br />输出: [[1]]<br />示例 5：</p>\n<p>输入: candidates = [1], target = 2<br />输出: [[1,1]]<br />&nbsp;</p>\n<p>提示：</p>\n<p>1 &lt;= candidates.length &lt;= 30<br />1 &lt;= candidates[i] &lt;= 200<br />candidate 中的每个元素都是独一无二的。<br />1 &lt;= target &lt;= 500</p>\n<h2>Python:</h2>\n<hr />\n<pre class=\"language-python\"><code>class Solution:\n    def combinationSum(self, candidates: List[int], target: int) -&gt; List[List[int]]:\n        res = []\n        tem = []\n        candidates.sort()\n\n        def backStack(begin, tar):\n            if tar == 0:\n                res.append(tem[:])\n            else:\n                for i in range(begin, len(candidates)):\n                    # 剪枝\n                    if tar &lt; candidates[i]:\n                        break\n                    tem.append(candidates[i])\n                    backStack(i, tar - candidates[i])\n                    tem.pop()\n\n        backStack(0, target)\n        return res</code></pre>\n<h2>Java:</h2>\n<pre class=\"language-java\"><code> List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();\n    List&lt;Integer&gt; tem = new ArrayList&lt;&gt;();\n    public List&lt;List&lt;Integer&gt;&gt; combinationSum(int[] candidates, int target) {\n        Arrays.sort(candidates);\n        backStack(candidates, target, 0);\n        return res;\n    }\n\n    private void backStack(int[] candidates, int temTarget, int i) {\n        if (temTarget == 0) {\n            res.add(new ArrayList&lt;&gt;(tem));\n            return;\n        } else if (temTarget &lt; 0) {\n            return;\n        }\n        for (int j = i; j &lt; candidates.length; j++) {\n            if (temTarget - candidates[j] &lt; 0) {\n                break;\n            }\n            tem.add(candidates[j]);\n            backStack(candidates, temTarget - candidates[j], j);\n            tem.remove(tem.size() - 1);\n        }\n    }</code></pre>', '39. 组合总和', '', 0, 1, 1, 103, 2, '2021-08-24 20:48:16', '2021-08-24 20:48:16');
INSERT INTO `t_blog` VALUES (118, '56. 合并区间', '<p>&nbsp;</p>\n<p>以数组 intervals 表示若干个区间的集合，其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间，并返回一个不重叠的区间数组，该数组需恰好覆盖输入中的所有区间。</p>\n<p>&nbsp;</p>\n<p>示例 1：</p>\n<p>输入：intervals = [[1,3],[2,6],[8,10],[15,18]]<br />输出：[[1,6],[8,10],[15,18]]<br />解释：区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].<br />示例&nbsp;2：</p>\n<p>输入：intervals = [[1,4],[4,5]]<br />输出：[[1,5]]<br />解释：区间 [1,4] 和 [4,5] 可被视为重叠区间。<br />&nbsp;</p>\n<p>提示：</p>\n<p>1 &lt;= intervals.length &lt;= 104<br />intervals[i].length == 2<br />0 &lt;= starti &lt;= endi &lt;= 104</p>\n<p>&nbsp;</p>\n<pre class=\"language-python\"><code>class Solution:\n    def merge(self, intervals: List[List[int]]) -&gt; List[List[int]]:\n        intervals.sort(key=lambda x: x[0])\n        res = []\n        for i in intervals:\n            if not res or i[0] &gt; res[-1][1]:\n                # 如果列表为空，或者当前区间与上一区间不重合，直接添加\n                res.append(i)\n            elif i[1] &gt; res[-1][1]:\n                # 将上一区间延长\n                res[-1][1] = i[1]\n        return res</code></pre>', '56. 合并区间', '', 0, 1, 1, 102, 2, '2021-08-24 21:25:44', '2021-08-24 21:25:44');
INSERT INTO `t_blog` VALUES (119, '11. 盛最多水的容器', '<p>给你 n 个非负整数 a1，a2，...，an，每个数代表坐标中的一个点&nbsp;(i,&nbsp;ai) 。在坐标内画 n 条垂直线，垂直线 i&nbsp;的两个端点分别为&nbsp;(i,&nbsp;ai) 和 (i, 0) 。找出其中的两条线，使得它们与&nbsp;x&nbsp;轴共同构成的容器可以容纳最多的水。</p>\n<p>说明：你不能倾斜容器。</p>\n<p>输入：[1,8,6,2,5,4,8,3,7]<br />输出：49 <br />解释：图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下，容器能够容纳水（表示为蓝色部分）的最大值为&nbsp;49。<br />示例 2：</p>\n<p>输入：height = [1,1]<br />输出：1<br />示例 3：</p>\n<p>输入：height = [4,3,2,1,4]<br />输出：16<br />示例 4：</p>\n<p>输入：height = [1,2,1]<br />输出：2<br />&nbsp;</p>\n<p>提示：</p>\n<p>n = height.length<br />2 &lt;= n &lt;= 3 * 104<br />0 &lt;= height[i] &lt;= 3 * 104</p>\n<p>&nbsp;</p>\n<p><br /><img src=\"https://aliyun-lc-upload.oss-cn-hangzhou.aliyuncs.com/aliyun-lc-upload/uploads/2018/07/25/question_11.jpg\" /></p>\n<pre class=\"language-python\"><code>class Solution:\n    def maxArea(self, height: List[int]) -&gt; int:\n        # 双指针 小的移动,结果才可能变大\n        left, right = 0, len(height) - 1\n        res = 0\n        while (left &lt; right):\n            tem = min(height[left], height[right]) * (right - left)\n            res = max(res, tem)\n            if height[left] &lt; height[right]:\n                left += 1\n            else:\n                right -= 1\n        return res</code></pre>', '11. 盛最多水的容器', '', 0, 1, 1, 123, 2, '2021-08-25 22:35:55', '2021-08-25 22:35:55');
INSERT INTO `t_blog` VALUES (120, '42. 接雨水', '<p>给定&nbsp;<em>n</em>&nbsp;个非负整数表示每个宽度为 1 的柱子的高度图，计算按此排列的柱子，下雨之后能接多少雨水。</p>\n<p><img src=\"https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/22/rainwatertrap.png\" /></p>\n<p>输入：height = [0,1,0,2,1,0,1,3,2,1,2,1]<br />输出：6<br />解释：上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图，在这种情况下，可以接 6 个单位的雨水（蓝色部分表示雨水）。 <br />示例 2：</p>\n<p>输入：height = [4,2,0,3,2,5]<br />输出：9<br />&nbsp;</p>\n<p>提示：</p>\n<p>n == height.length<br />0 &lt;= n &lt;= 3 * 104<br />0 &lt;= height[i] &lt;= 105</p>\n<hr />\n<h2>单调栈</h2>\n<pre class=\"language-python\"><code>class Solution:\n    def trap(self, height: List[int]) -&gt; int:\n        stack = []\n        res = 0\n        for i in range(len(height)):\n            if stack == [] or height[i] &lt; height[stack[-1]]:\n                stack.append(i)\n            else:\n                while height[i] &gt;= height[stack[-1]]:\n                    j = stack.pop()\n                    if not stack:\n                        break\n                    left = stack[-1]\n                    cur_width = i - 1 - left\n                    # 当前容量 需要两边最低者确定\n                    cur_height = min(height[left], height[i]) - height[j]\n                    res += cur_width * cur_height\n                stack.append(i)\n        return res</code></pre>\n<h2>双指针</h2>\n<pre class=\"language-python\"><code>class Solution:\n    def trap(self, height: List[int]) -&gt; int:\n        left, right = 0, len(height) - 1\n        max_left, max_right = 0, len(height) - 1\n        res = 0\n        while left &lt; right:\n            if height[left] &lt; height[right]:\n                if height[left] &lt; height[max_left]:  # 当前容量取决于两边中低位（height[max_left]）\n                    res += height[max_left] - height[left]\n                else:\n                    max_left = left\n                left += 1\n            else:\n                if height[right] &lt; height[max_right]:  # 当前容量取决于两边中低位（height[max_right]）\n                    res += height[max_right] - height[right]\n                else:\n                    max_right = right\n                right -= 1\n        return res</code></pre>', '42. 接雨水', '', 0, 1, 1, 110, 2, '2021-08-29 21:36:18', '2021-08-29 21:36:18');
INSERT INTO `t_blog` VALUES (121, '106. 从中序与后序遍历序列构造二叉树', '<p>根据一棵树的中序遍历与后序遍历构造二叉树。</p>\n<p>注意:<br />你可以假设树中没有重复的元素。</p>\n<p>例如，给出</p>\n<p>中序遍历 inorder =&nbsp;[9,3,15,20,7]<br />后序遍历 postorder = [9,15,7,20,3]<br />返回如下的二叉树：</p>\n<p>3<br />/ \\<br />9 20<br />/ \\<br />15 7</p>\n<p>&nbsp;</p>\n<pre class=\"language-python\"><code>class Solution:\n    def buildTree(self, inorder: List[int], postorder: List[int]) -&gt; TreeNode:\n        # 建立（元素，下标）键值对的哈希表\n        idx_map = {val: idx for idx, val in enumerate(inorder)}\n\n        def backStack(in_begin, in_end):\n            if in_begin &gt; in_end:\n                return\n            if not postorder:\n                return\n            # 后续遍历 最后一个为根节点\n            tem_root = TreeNode(postorder.pop())\n            index = idx_map[tem_root.val]\n            # 先右子树（后续遍历 下一次pop 一定先存在右子树，后左子树）\n            tem_root.right = backStack(index + 1, in_end)\n            # 后左子树\n            tem_root.left = backStack(in_begin, index - 1)\n            return tem_root\n\n        return backStack(0, len(inorder) - 1)</code></pre>', '106. 从中序与后序遍历序列构造二叉树', '', 0, 1, 1, 96, 2, '2021-08-31 22:34:26', '2021-08-31 22:34:26');
INSERT INTO `t_blog` VALUES (122, '178. 子集', '<p>给你一个整数数组&nbsp;<code>nums</code>&nbsp;，数组中的元素&nbsp;<strong>互不相同</strong>&nbsp;。返回该数组所有可能的子集（幂集）。</p>\n<p>解集&nbsp;<strong>不能</strong>&nbsp;包含重复的子集。你可以按&nbsp;<strong>任意顺序</strong>&nbsp;返回解集。</p>\n<p>示例 1：</p>\n<p>输入：nums = [1,2,3]<br />输出：[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]<br />示例 2：</p>\n<p>输入：nums = [0]<br />输出：[[],[0]]<br />&nbsp;</p>\n<p>提示：</p>\n<p>1 &lt;= nums.length &lt;= 10<br />-10 &lt;= nums[i] &lt;= 10<br />nums 中的所有元素 互不相同</p>\n<hr />\n<p>回溯类的问题 先画图 后编码</p>\n<p>每个节点上 记录当前状态</p>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/9284c48ffe1e4ca8b342472146dc10e9.png\" /></p>\n<pre class=\"language-python\"><code>class Solution:\n    def subsets(self, nums: List[int]) -&gt; List[List[int]]:\n        res = []\n        tem = []\n        \n        def backStack(begin):\n            res.append(tem[:])\n            for i in range(begin, len(nums)):\n                tem.append(nums[i])\n                backStack(i + 1)\n                tem.pop()\n\n        backStack(0)\n        return res</code></pre>', '78. 子集', '', 0, 1, 1, 141, 2, '2021-09-01 21:54:57', '2021-09-01 21:54:57');
INSERT INTO `t_blog` VALUES (123, '股票问题 ', '<h2>121. 买卖股票的最佳时机</h2>\n<div>\n<p>给定一个数组 prices ，它的第&nbsp;i 个元素&nbsp;prices[i] 表示一支给定股票第 i 天的价格。</p>\n<p>你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。</p>\n<p>返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。</p>\n<p>&nbsp;</p>\n<p>示例 1：</p>\n<p>输入：[7,1,5,3,6,4]<br />输出：5<br />解释：在第 2 天（股票价格 = 1）的时候买入，在第 5 天（股票价格 = 6）的时候卖出，最大利润 = 6-1 = 5 。<br />注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格；同时，你不能在买入前卖出股票。<br />示例 2：</p>\n<p>输入：prices = [7,6,4,3,1]<br />输出：0<br />解释：在这种情况下, 没有交易完成, 所以最大利润为 0。</p>\n<p>&nbsp;</p>\n</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>提示：</div>\n<div>&nbsp;</div>\n<div>1 &lt;= prices.length &lt;= 105</div>\n<div>0 &lt;= prices[i] &lt;= 104</div>\n<div>&nbsp;</div>\n<div>买入第x天的股票,如果后续第i天 价格比x天价格低 最大利润一定是 \"当前利润(x天买入时)\"和 \"第i天买入的最大利润\" 中最大者<br />x初始值为0</div>\n<div>&nbsp;</div>\n<pre class=\"language-python\"><code>class Solution:\n    def maxProfit(self, prices: List[int]) -&gt; int:\n        if not prices:\n            return 0\n        res = 0\n        tem_min = prices[0]\n        for cur in prices:\n            # 遇到高点 计算当前最大利润\n            if cur &gt; tem_min:\n                res = max(res, cur - tem_min)\n            # 遇到低点 更新买入价格\n            else:\n                tem_min = cur\n        return res</code></pre>\n<div class=\"css-xfm0cl-Container eugt34i0\" data-show-mask=\"false\">\n<h2 class=\"css-1e1vffy-Tools e1o5n5iy0\">&nbsp;</h2>\n<h2 class=\"css-1e1vffy-Tools e1o5n5iy0\">122.买卖股票的最佳时机 II</h2>\n</div>\n<div class=\"content__1Y2H\">\n<div class=\"notranslate\">\n<p>给定一个数组&nbsp;<code>prices</code>&nbsp;，其中&nbsp;<code>prices[i]</code>&nbsp;是一支给定股票第&nbsp;<code>i</code>&nbsp;天的价格。</p>\n<p>设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易（多次买卖一支股票）。</p>\n<p><strong>注意：</strong>你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。</p>\n<p>&nbsp;</p>\n<p><strong>示例 1:</strong></p>\n<pre><strong>输入:</strong> prices = [7,1,5,3,6,4]\n<strong>输出:</strong> 7\n<strong>解释:</strong> 在第 2 天（股票价格 = 1）的时候买入，在第 3 天（股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。\n&nbsp;    随后，在第 4 天（股票价格 = 3）的时候买入，在第 5 天（股票价格 = 6）的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。\n</pre>\n<p><strong>示例 2:</strong></p>\n<pre><strong>输入:</strong> prices = [1,2,3,4,5]\n<strong>输出:</strong> 4\n<strong>解释:</strong> 在第 1 天（股票价格 = 1）的时候买入，在第 5 天 （股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。\n&nbsp;    注意你不能在第 1 天和第 2 天接连购买股票，之后再将它们卖出。因为这样属于同时参与了多笔交易，你必须在再次购买前出售掉之前的股票。\n</pre>\n<p><strong>示例&nbsp;3:</strong></p>\n<pre><strong>输入:</strong> prices = [7,6,4,3,1]\n<strong>输出:</strong> 0\n<strong>解释:</strong> 在这种情况下, 没有交易完成, 所以最大利润为 0。</pre>\n<p>&nbsp;</p>\n<p><strong>提示：</strong></p>\n<ul>\n<li><code>1 &lt;= prices.length &lt;= 3 * 10<sup>4</sup></code></li>\n<li><code>0 &lt;= prices[i] &lt;= 10<sup>4</sup></code></li>\n</ul>\n</div>\n</div>\n<p>状态: 不持有股票-&gt;持有股票-&gt;不持有股票-&gt;持有股票...&nbsp; &nbsp;</p>\n<p>持有到卖出当天 无法买入！如果继续涨 不能卖</p>\n<p>不持有股票时 每天都可以选择&ldquo;买入&rdquo;或&ldquo;不买入&rdquo;&nbsp; 代码中：手里没股票就买入 第二天跌了 就卖出第一天的买入第二天的 （等价于第一天没买入 第二天买入）</p>\n<pre class=\"language-python\"><code>class Solution:\n    def maxProfit(self, prices: List[int]) -&gt; int:\n        res = 0\n        # 当前是否持有股票\n        has_ticket = False\n        # 上次买股票花费\n        pre_cost = 0\n\n        for i in range(len(prices)):\n            cur = prices[i]\n            if has_ticket:  # 当前持有股票\n                if cur &gt; pre_cost:  # 有利润 可以选择现在卖 或 等等卖\n                    if i + 1 &lt; len(prices) and prices[i + 1] &gt; cur:  # 如果还会涨 就不卖 到最高点再卖\n                        continue\n                    else:  # 后面跌了 今天卖\n                        res += cur - pre_cost\n                        has_ticket = False\n                else:  # 股票跌了,就当上次没买 买这次的 (骚操作)\n                    pre_cost = cur\n            else:  # 当没有股票\n                # 买入\n                pre_cost = cur\n                has_ticket = True\n        return res</code></pre>\n<h2>714. 买卖股票的最佳时机含手续费</h2>\n<p>给定一个整数数组&nbsp;prices，其中第&nbsp;i&nbsp;个元素代表了第&nbsp;i&nbsp;天的股票价格 ；整数&nbsp;fee 代表了交易股票的手续费用。</p>\n<p>你可以无限次地完成交易，但是你每笔交易都需要付手续费。如果你已经购买了一个股票，在卖出它之前你就不能再继续购买股票了。</p>\n<p>返回获得利润的最大值。</p>\n<p>注意：这里的一笔交易指买入持有并卖出股票的整个过程，每笔交易你只需要为支付一次手续费。</p>\n<p>&nbsp;</p>\n<p>示例 1：</p>\n<p>输入：prices = [1, 3, 2, 8, 4, 9], fee = 2<br />输出：8<br />解释：能够达到的最大利润: <br />在此处买入&nbsp;prices[0] = 1<br />在此处卖出 prices[3] = 8<br />在此处买入 prices[4] = 4<br />在此处卖出 prices[5] = 9<br />总利润:&nbsp;((8 - 1) - 2) + ((9 - 4) - 2) = 8<br />示例 2：</p>\n<p>输入：prices = [1,3,7,5,10,3], fee = 3<br />输出：6<br />&nbsp;</p>\n<p>提示：</p>\n<p>1 &lt;= prices.length &lt;= 5 * 104<br />1 &lt;= prices[i] &lt; 5 * 104<br />0 &lt;= fee &lt; 5 * 104</p>\n<hr />\n<h3>动态规划</h3>\n<p>定义二维数组 dp[n][2]dp[n][2]：</p>\n<p>dp[i][0]dp[i][0] 表示第 i 天不持有可获得的最大利润；<br />dp[i][1]dp[i][1] 表示第 i 天持有可获得的最大利润（注意是第 i 天持有，而不是第 i 天买入）。<br />定义状态转移方程：</p>\n<p>不持有：dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee)dp[i][0]=max(dp[i&minus;1][0],dp[i&minus;1][1]+prices[i]&minus;fee)</p>\n<p>对于今天不持有，可以从两个状态转移过来：1. 昨天也不持有；2. 昨天持有，今天卖出。两者取较大值。</p>\n<p>持有：dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])dp[i][1]=max(dp[i&minus;1][1],dp[i&minus;1][0]&minus;prices[i])</p>\n<p>对于今天持有，可以从两个状态转移过来：1. 昨天也持有；2. 昨天不持有，今天买入。两者取较大值。</p>\n<p>dp[i]只依赖dp[i-1]所以 dp数组可以优化为2个状态：</p>\n<pre class=\"language-python\"><code>class Solution:\n    def maxProfit(self, prices: List[int], fee: int) -&gt; int:\n        # 假设手续费是卖出时 付款\n        no_hold, hold = 0, -prices[0]\n        for i in range(1, len(prices)):\n            # i天持有 最大利润：继续持有 or [i-1天不持有]+买入\n            # i天不持有 最大利润：继续不持有 or [i-1天持有]+卖出\n            no_hold, hold = max(no_hold, hold + prices[i] - fee), max(hold, no_hold - prices[i])\n        return no_hold</code></pre>', '121. 买卖股票的最佳时机 122.买卖股票的最佳时机 II 714. 买卖股票的最佳时机含手续费', '', 0, 1, 1, 118, 2, '2021-09-04 15:43:44', '2021-09-04 15:43:44');
INSERT INTO `t_blog` VALUES (124, '142. 环形链表 II', '<p>给定一个链表，返回链表开始入环的第一个节点。&nbsp;如果链表无环，则返回&nbsp;null。</p>\n<p>为了表示给定链表中的环，我们使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。 如果 pos 是 -1，则在该链表中没有环。注意，pos 仅仅是用于标识环的情况，并不会作为参数传递到函数中。</p>\n<p>说明：不允许修改给定的链表。</p>\n<p>进阶：</p>\n<p>你是否可以使用 O(1) 空间解决此题？</p>\n<p>示例 1：</p>\n<p><img src=\"https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist.png\" /></p>\n<p>输入：head = [3,2,0,-4], pos = 1<br />输出：返回索引为 1 的链表节点<br />解释：链表中有一个环，其尾部连接到第二个节点。<br />示例&nbsp;2：</p>\n<p><strong><img src=\"https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test2.png\" /></strong></p>\n<p>输入：head = [1,2], pos = 0<br />输出：返回索引为 0 的链表节点<br />解释：链表中有一个环，其尾部连接到第一个节点。<br />示例 3：</p>\n<p><img src=\"https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test3.png\" /></p>\n<p>输入：head = [1], pos = -1<br />输出：返回 null<br />解释：链表中没有环</p>\n<p><strong>提示：</strong></p>\n<ul>\n<li>链表中节点的数目范围在范围&nbsp;<code>[0, 10<sup>4</sup>]</code>&nbsp;内</li>\n<li><code>-10<sup>5</sup>&nbsp;&lt;= Node.val &lt;= 10<sup>5</sup></code></li>\n<li><code>pos</code>&nbsp;的值为&nbsp;<code>-1</code>&nbsp;或者链表中的一个有效索引</li>\n</ul>\n<hr />\n<p>我们先设置慢指针 slow 和快指针fast ，慢指针每次走一步，快指针每次走两步，根据「Floyd 判圈算法」两个指针在有环的情况下一定会相遇，此时我们再将slow 放置起点 0，两个指针每次同时移动一步，相遇的点就是答案。</p>\n<p>这里简单解释为什么后面将 slow 放置起点后移动相遇的点就一定是答案了。假设环长为 L，从起点到环的入口的步数是 a，从环的入口继续走 b 步到达相遇位置，从相遇位置继续走 c 步回到环的入口，则有 b+c=L，其中 L、a、b、c 都是正整数。根据上述定义，慢指针走了 a+b 步，快指针走了 2(a+b) 步。从另一个角度考虑，在相遇位置，快指针比慢指针多走了若干圈，因此快指针走的步数还可以表示成 a+b+kL，其中 k 表示快指针在环上走的圈数。联立等式，可以得到</p>\n<p>2(a+b)=a+b+kL</p>\n<p>解得 a=kL-ba=kL&minus;b，整理可得</p>\n<p>a=(k-1)L+(L-b)=(k-1)L+c</p>\n<p>从上述等式可知，如果慢指针从起点出发，快指针从相遇位置出发，每次两个指针都移动一步，则慢指针走了 a 步之后到达环的入口，快指针在环里走了 k-1 圈之后又走了 c 步，由于从相遇位置继续走 c 步即可回到环的入口，因此快指针也到达环的入口。两个指针在环的入口相遇，相遇点就是答案。</p>\n<p><img src=\"https://assets.leetcode-cn.com/solution-static/142/142_fig1.png\" alt=\"fig1\" /></p>\n<pre class=\"language-python\"><code>class Solution:\n    def detectCycle(self, head: ListNode) -&gt; ListNode:\n        fast, slow = head, head\n        # 找到相遇点\n        while True:\n            if not fast or not fast.next:\n                return \n            fast = fast.next.next\n            slow = slow.next\n            if slow == fast:\n                break\n        tem = head\n        #找到环入口\n        while True:\n            if slow == tem:\n                return slow\n            slow = slow.next\n            tem = tem.next</code></pre>\n<p><a href=\"https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/huan-xing-lian-biao-ii-by-leetcode-solution/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '142. 环形链表 II', '', 0, 1, 1, 110, 2, '2021-09-09 22:03:57', '2021-09-09 22:03:57');
INSERT INTO `t_blog` VALUES (125, '337. 打家劫舍 III', '<p>在上次打劫完一条街道之后和一圈房屋后，小偷又发现了一个新的可行窃的地区。这个地区只有一个入口，我们称之为&ldquo;根&rdquo;。 除了&ldquo;根&rdquo;之外，每栋房子有且只有一个&ldquo;父&ldquo;房子与之相连。一番侦察之后，聪明的小偷意识到&ldquo;这个地方的所有房屋的排列类似于一棵二叉树&rdquo;。 如果两个直接相连的房子在同一天晚上被打劫，房屋将自动报警。</p>\n<p>计算在不触动警报的情况下，小偷一晚能够盗取的最高金额。</p>\n<p>输入: [3,2,3,null,3,null,1]</p>\n<p>3<br />/ \\<br />2 3<br />\\ \\ <br />3 1</p>\n<p>输出: 7 <br />解释:&nbsp;小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.</p>\n<p>&nbsp;</p>\n<p>输入: [3,4,5,1,3,null,1]</p>\n<p>&nbsp; 3<br />/ \\<br />4 5<br />/ \\ \\ <br />1 3 1</p>\n<p>输出: 9<br />解释:&nbsp;小偷一晚能够盗取的最高金额&nbsp;= 4 + 5 = 9.</p>\n<h4 id=\"方法一：动态规划\">动态规划</h4>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/8b43c5544b09490c8b12e54c527d7d43.png\" /></p>\n<pre class=\"language-python\"><code>class Solution:\n    def rob(self, root: TreeNode) -&gt; int:\n        #f[node]:选中node时最大利润\n        #g[node]:不选node时最大利润\n        f = {}\n        g = {}\n        f[None] = 0\n        g[None] = 0\n        if not root:\n            return 0\n\n        def dfs(tem):\n            if not tem:\n                return\n            dfs(tem.left)\n            dfs(tem.right)\n            f[tem] = tem.value + g[tem.left] + g[tem.right]\n            g[tem] = max(f[tem.left], g[tem.left]) + max(f[tem.right], g[tem.right])\n\n        dfs(root)\n        return max(f[root], g[root])</code></pre>\n<p><a href=\"https://leetcode-cn.com/problems/house-robber-iii/solution/da-jia-jie-she-iii-by-leetcode-solution/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '337. 打家劫舍 III', '', 0, 1, 1, 155, 2, '2021-09-15 20:26:22', '2021-09-15 20:26:22');
INSERT INTO `t_blog` VALUES (126, '448. 找到所有数组中消失的数字', '<p>给你一个含 n 个整数的数组 nums ，其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字，并以数组的形式返回结果。</p>\n<p>示例 1：</p>\n<p>输入：nums = [4,3,2,7,8,2,3,1]<br />输出：[5,6]<br />示例 2：</p>\n<p>输入：nums = [1,1]<br />输出：[2]<br />&nbsp;</p>\n<p>提示：</p>\n<p>n == nums.length<br />1 &lt;= n &lt;= 105<br />1 &lt;= nums[i] &lt;= n<br />进阶：你能在不使用额外空间且时间复杂度为 O(n) 的情况下解决这个问题吗? 你可以假定返回的数组不算在额外空间内。</p>\n<p>&nbsp;</p>\n<pre class=\"language-python\"><code>class Solution:\n    def findDisappearedNumbers(self, nums: List[int]) -&gt; List[int]:\n        res = []\n        n = len(nums)\n        for num in nums:\n            #数可能已经被增加过，因此需要对 n 取模来还原出它本来的值\n            i = (num - 1) % n\n            #每遇到一个数 x，就让nums[x&minus;1] 增加 n\n            nums[i] += n\n        for j in range(0,n):\n            if nums[j] &lt;= n:\n                res.append(j+1)\n        return res</code></pre>', '448. 找到所有数组中消失的数字', '', 0, 1, 1, 123, 2, '2021-09-25 20:40:14', '2021-09-25 20:40:14');
INSERT INTO `t_blog` VALUES (127, '25. K 个一组翻转链表', '<p>给你一个链表，每&nbsp;k&nbsp;个节点一组进行翻转，请你返回翻转后的链表。</p>\n<p>k&nbsp;是一个正整数，它的值小于或等于链表的长度。</p>\n<p>如果节点总数不是&nbsp;k&nbsp;的整数倍，那么请将最后剩余的节点保持原有顺序。</p>\n<p>进阶：</p>\n<p>你可以设计一个只使用常数额外空间的算法来解决此问题吗？<br />你不能只是单纯的改变节点内部的值，而是需要实际进行节点交换。</p>\n<p><strong>示例 1：</strong></p>\n<p><img src=\"https://assets.leetcode.com/uploads/2020/10/03/reverse_ex1.jpg\" /></p>\n<pre><strong>输入：</strong>head = [1,2,3,4,5], k = 2\n<strong>输出：</strong>[2,1,4,3,5]<br /><br /><strong>示例 2：</strong><br /><br /><img src=\"https://assets.leetcode.com/uploads/2020/10/03/reverse_ex2.jpg\" /></pre>\n<p>示例 3：</p>\n<p>输入：head = [1,2,3,4,5], k = 1<br />输出：[1,2,3,4,5]<br />示例 4：</p>\n<p>输入：head = [1], k = 1<br />输出：[1]<br />提示：</p>\n<p>列表中节点的数量在范围 sz 内<br />1 &lt;= sz &lt;= 5000<br />0 &lt;= Node.val &lt;= 1000<br />1 &lt;= k &lt;= sz</p>\n<hr />\n<p>思路使用长度为k的数组 辅助,每填充满数组反转一次</p>\n<pre class=\"language-java\"><code>/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    public  ListNode reverseKGroup(ListNode head, int k) {\n        ListNode newHead = new ListNode();\n        newHead.next = head;\n        ListNode[] list = new ListNode[k];\n        int i = -1;\n        ListNode tem = newHead.next;\n        ListNode pre = newHead;\n        while (tem != null) {\n            list[++i] = tem;\n            tem = tem.next;\n            if (i == k - 1) {\n                //反转一次\n                ListNode next = list[i].next;\n                pre.next = list[k - 1];\n                for (int j = k - 1; j &gt; 0; j--) {\n                    list[j].next = list[j - 1];\n                }\n                list[0].next = next;\n                pre = list[0];\n                i = -1;\n            }\n        }\n        return newHead.next;\n\n    }\n}</code></pre>', '25. K 个一组翻转链表', '', 0, 1, 1, 196, 2, '2021-09-28 21:57:21', '2021-09-28 21:57:21');
INSERT INTO `t_blog` VALUES (128, '215. 数组中的第K个最大元素', '<p>给定整数数组 nums 和整数 k，请返回数组中第 k 个最大的元素。</p>\n<p>请注意，你需要找的是数组排序后的第 k 个最大的元素，而不是第 k 个不同的元素。</p>\n<p>&nbsp;</p>\n<p>示例 1:</p>\n<p>输入: [3,2,1,5,6,4] 和 k = 2<br />输出: 5<br />示例&nbsp;2:</p>\n<p>输入: [3,2,3,1,2,4,5,5,6] 和 k = 4<br />输出: 4<br />&nbsp;</p>\n<p>提示：</p>\n<p>1 &lt;= k &lt;= nums.length &lt;= 104<br />-104&nbsp;&lt;= nums[i] &lt;= 104</p>\n<hr />\n<p>维护k个长度的 小顶堆，堆顶为要求的值（topK用小顶堆）</p>\n<pre class=\"language-java\"><code>class Solution {\n    public int findKthLargest(int[] nums, int k) {\n        // 使用数组构建小顶堆\n        int[] top = new int[k];\n        // 将 nums 数组中的前 k 个元素取出来\n        for (int i = 0; i &lt; k; i++) {\n            top[i] = nums[i];\n        }\n        // 先建堆，然后依次比较剩余元素与堆顶元素的大小，比堆顶大的，说明它应该在堆中出现，则用它来替换掉堆顶元素，然后沉降。\n        buildHeap(top);\n        for (int j = k; j &lt; nums.length; j++) {\n            int temp = top[0];\n            if (temp &lt; nums[j]) {\n                setTop(top, nums[j]);\n            }\n        }\n        return top[0];\n    }\n\n    private void setTop(int[] nums, int num) {\n        nums[0] = num;\n        heapify(nums, 0, nums.length);\n    }\n\n    private void buildHeap(int[] top) {\n        //找到第一个非叶子节点，遍历调整\n        int startHeapify = (top.length - 1) / 2;\n        for (int i = startHeapify; i &gt;= 0; i--) {\n            heapify(top, i, top.length);\n        }\n    }\n    public void heapify(int[] nums, int index, int length) {\n        //左孩子，右孩子\n        int left = (index &lt;&lt; 1) + 1;\n        int right = (index &lt;&lt; 1) + 2;\n        int min = index;\n        //找到3个中最小的 (根左右)\n        if (left &lt; length &amp;&amp; nums[left] &lt; nums[min]) {\n            min = left;\n        }\n        if (right &lt; length &amp;&amp; nums[right] &lt; nums[min]) {\n            min = right;\n        }\n        //如果当前不是3个中最小的 交换位置后 递归\n        if (min != index) {\n            swap(nums, min, index);\n            heapify(nums, min, length);\n        }\n    }\n\n    private void swap(int[] nums, int min, int index) {\n        int a = nums[min];\n        nums[min] = nums[index];\n        nums[index] = a;\n    }\n}</code></pre>\n<p><a href=\"https://leetcode-cn.com/problems/kth-largest-element-in-an-array/solution/shu-zu-zhong-de-di-kge-zui-da-yuan-su-ja-lo3g/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '215. 数组中的第K个最大元素 堆排序', '', 0, 1, 1, 148, 2, '2021-10-01 21:34:30', '2021-10-01 21:34:30');
INSERT INTO `t_blog` VALUES (129, '二叉树的前中后序遍历 非递归', '<h2>二叉树前序遍历</h2>\n<p>前序遍历顺序：根-&gt;左-&gt;右</p>\n<pre class=\"language-java\"><code>    public List&lt;Integer&gt; preorderTraversal(TreeNode root) {\n        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();\n        Stack&lt;TreeNode&gt; stack = new Stack&lt;&gt;();\n        stack.add(root);\n        while (!stack.isEmpty()) {\n            TreeNode tem = stack.pop();\n            if (tem != null) {\n                //访问节点同时 将右，左子节点放栈顶\n                res.add(tem.val);\n                stack.add(tem.right);\n                stack.add(tem.left);\n            }\n        }\n        return res;\n    }</code></pre>\n<h2>二叉树中序遍历</h2>\n<p>前序遍历顺序：左-&gt;根-&gt;右</p>\n<pre class=\"language-java\"><code> public List&lt;Integer&gt; inorderTraversal(TreeNode root) {\n        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();\n        Stack&lt;TreeNode&gt; stack = new Stack&lt;&gt;();\n        TreeNode tem = root;\n        while (!stack.isEmpty() || tem != null) {\n            while (tem != null) {\n                stack.add(tem);\n                tem = tem.left;\n            }\n            //此时栈顶节点左子树访问完了\n            TreeNode pop = stack.pop();\n            if (pop != null) {\n                res.add(pop.val);\n                //右子树\n                tem = pop.right;\n            }\n        }\n        return res;\n    }</code></pre>\n<h2>二叉树后序遍历</h2>\n<p>前序遍历顺序：左-&gt;右-&gt;根</p>\n<pre class=\"language-java\"><code> public List&lt;Integer&gt; postorderTraversal(TreeNode root) {\n        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();\n        Stack&lt;TreeNode&gt; stack = new Stack&lt;&gt;();\n        TreeNode tem = root;\n        //标识\n        TreeNode last = null;\n        while (!stack.isEmpty() || tem != null) {\n            while (tem != null) {\n                stack.add(tem);\n                tem = tem.left;\n            }\n            last = null;\n            while (!stack.isEmpty()) { //右子树返回，要循环\n                TreeNode pop = stack.pop();\n                if (pop.right == last) {\n                    //右子树返回,访问\n                    res.add(pop.val);\n                    last = pop;\n                } else {\n                    //左子树返回,不访问\n                    stack.add(pop);\n                    tem = pop.right;\n                    break;\n                }\n            }\n        }\n        return res;\n    }</code></pre>', '二叉树的前中后序遍历 非递归', '', 0, 1, 1, 160, 2, '2021-10-09 16:33:46', '2021-10-09 17:22:16');
INSERT INTO `t_blog` VALUES (130, '22. 括号生成', '<p>数字 n&nbsp;代表生成括号的对数，请你设计一个函数，用于能够生成所有可能的并且 有效的 括号组合。</p>\n<p>有效括号组合需满足：左括号必须以正确的顺序闭合。</p>\n<p>&nbsp;</p>\n<p>示例 1：</p>\n<p>输入：n = 3<br />输出：[\"((()))\",\"(()())\",\"(())()\",\"()(())\",\"()()()\"]<br />示例 2：</p>\n<p>输入：n = 1<br />输出：[\"()\"]<br />&nbsp;</p>\n<p>提示：</p>\n<p>1 &lt;= n &lt;= 8</p>\n<h2>回溯</h2>\n<p><img src=\"https://pic.leetcode-cn.com/7ec04f84e936e95782aba26c4663c5fe7aaf94a2a80986a97d81574467b0c513-LeetCode%20%E7%AC%AC%2022%20%E9%A2%98%EF%BC%9A%E2%80%9C%E6%8B%AC%E5%8F%B7%E7%94%9F%E5%87%BA%E2%80%9D%E9%A2%98%E8%A7%A3%E9%85%8D%E5%9B%BE.png\" /></p>\n<pre class=\"language-java\"><code>class Solution {\n    List&lt;String&gt; res = new ArrayList&lt;&gt;();\n\n    public List&lt;String&gt; generateParenthesis(int n) {\n        backtrack(new StringBuilder(), n, n);\n        return res;\n    }\n\n     private void backtrack(StringBuilder cur, int left, int right) {\n        if (left == 0 &amp;&amp; right == 0) {\n            res.add(cur.toString());\n            return;\n        }\n        if (left &gt; 0) {\n            backtrack(cur.append(\"(\"), left - 1, right);\n            cur.deleteCharAt(cur.length() - 1);\n        }\n        if (right&gt;0 &amp;&amp; left&lt;right) {//剪枝\n            backtrack(cur.append(\")\"), left, right - 1);\n            cur.deleteCharAt(cur.length() - 1);\n        }\n\n\n    }\n}</code></pre>', '22. 括号生成', '', 0, 1, 1, 149, 2, '2021-10-21 19:07:59', '2021-10-21 19:07:59');
INSERT INTO `t_blog` VALUES (131, '322. 零钱兑换', '<div>给你一个整数数组 coins ，表示不同面额的硬币；以及一个整数 amount ，表示总金额。</div>\n<div>&nbsp;</div>\n<div>计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额，返回 -1 。</div>\n<div>&nbsp;</div>\n<div>你可以认为每种硬币的数量是无限的。</div>\n<div>&nbsp;</div>\n<div>\n<div>示例 1：</div>\n<div>&nbsp;</div>\n<div>输入：coins = [1, 2, 5], amount = 11</div>\n<div>输出：3&nbsp;</div>\n<div>解释：11 = 5 + 5 + 1</div>\n<div>示例 2：</div>\n<div>&nbsp;</div>\n<div>输入：coins = [2], amount = 3</div>\n<div>输出：-1</div>\n<div>示例 3：</div>\n<div>&nbsp;</div>\n<div>输入：coins = [1], amount = 0</div>\n<div>输出：0</div>\n<div>示例 4：</div>\n<div>&nbsp;</div>\n<div>输入：coins = [1], amount = 1</div>\n<div>输出：1</div>\n<div>示例 5：</div>\n<div>&nbsp;</div>\n<div>输入：coins = [1], amount = 2</div>\n<div>输出：2</div>\n<div>&nbsp;</div>\n</div>\n<pre class=\"language-java\"><code>    public int coinChange(int[] coins, int amount) {\n        Arrays.sort(coins);//方便剪枝\n        int[] dp = new int[amount + 1];\n        dp[0] = 0;\n        for (int i = 1; i &lt; dp.length; i++) {\n            dp[i] = Integer.MAX_VALUE - 1;\n            for (int j = 0; j &lt; coins.length; j++) {\n                if (i &lt; coins[j]) {//剪枝\n                    break;\n                }\n                dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);\n            }\n        }\n\n        return dp[amount] == Integer.MAX_VALUE - 1 ? -1 : dp[amount];\n    }</code></pre>', '322. 零钱兑换', '', 0, 1, 1, 186, 2, '2021-10-22 18:49:10', '2021-10-22 18:49:10');
INSERT INTO `t_blog` VALUES (132, '34. 在排序数组中查找元素的第一个和最后一个位置', '<div>给定一个按照升序排列的整数数组 nums，和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。</div>\n<div>&nbsp;</div>\n<div>如果数组中不存在目标值 target，返回 [-1, -1]。</div>\n<div>&nbsp;</div>\n<div>进阶：</div>\n<div>&nbsp;</div>\n<div>你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗？</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>示例 1：</div>\n<div>&nbsp;</div>\n<div>输入：nums = [5,7,7,8,8,10], target = 8</div>\n<div>输出：[3,4]</div>\n<div>示例 2：</div>\n<div>&nbsp;</div>\n<div>输入：nums = [5,7,7,8,8,10], target = 6</div>\n<div>输出：[-1,-1]</div>\n<div>示例 3：</div>\n<div>&nbsp;</div>\n<div>输入：nums = [], target = 0</div>\n<div>输出：[-1,-1]</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>\n<pre class=\"language-java\"><code>class Solution {\n        public int[] searchRange(int[] nums, int target) {\n        if (nums == null || nums.length == 0) {\n            return new int[]{-1, -1};\n        }\n        int left = findLeft(nums, 0, nums.length - 1, target);\n        if (nums[left] != target) return new int[]{-1, -1};\n        int right = findRight(nums, left, nums.length - 1, target);\n        return new int[]{left, right};\n    }\n\n    int findLeft(int[] nums, int left, int right, int target) {\n        while (left &lt; right) {\n            int mid = (left + right) &gt;&gt; 1;\n            if (nums[mid] &gt;= target) {\n                right = mid;\n            } else {\n                left = mid + 1;\n            }\n        }\n        return left;\n    }\n\n    int findRight(int[] nums, int left, int right, int target) {\n        while (left &lt; right) {\n            int mid = (left + right + 1) &gt;&gt; 1;\n            if (nums[mid] &lt;= target) {\n                left = mid;\n            } else {\n                right = mid - 1;\n            }\n        }\n        return left;\n    }\n}\n</code></pre>\n</div>', '34. 在排序数组中查找元素的第一个和最后一个位置', '', 0, 1, 1, 109, 2, '2021-11-23 19:24:19', '2021-11-23 19:24:19');
INSERT INTO `t_blog` VALUES (133, '198. 打家劫舍 213. 打家劫舍 II', '<h2>198. 打家劫舍</h2>\n<div>你是一个专业的小偷，计划偷窃沿街的房屋。每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。</div>\n<div>&nbsp;</div>\n<div>给定一个代表每个房屋存放金额的非负整数数组，计算你 不触动警报装置的情况下 ，一夜之内能够偷窃到的最高金额。</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>示例 1：</div>\n<div>&nbsp;</div>\n<div>输入：[1,2,3,1]</div>\n<div>输出：4</div>\n<div>解释：偷窃 1 号房屋 (金额 = 1) ，然后偷窃 3 号房屋 (金额 = 3)。</div>\n<div>&nbsp; &nbsp; &nbsp;偷窃到的最高金额 = 1 + 3 = 4 。</div>\n<div>示例 2：</div>\n<div>&nbsp;</div>\n<div>输入：[2,7,9,3,1]</div>\n<div>输出：12</div>\n<div>解释：偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9)，接着偷窃 5 号房屋 (金额 = 1)。</div>\n<div>&nbsp; &nbsp; &nbsp;偷窃到的最高金额 = 2 + 9 + 1 = 12 。</div>\n<div>&nbsp;</div>\n<div>\n<pre class=\"language-java\"><code>public int rob(int[] nums) {\n        if (nums.length == 1) {\n            return nums[0];\n        }\n        if (nums.length == 2) {\n            return Math.max(nums[0], nums[1]);\n        }\n        //dp[i]=nums[i]+dp[i+2] or dp[i+1]\n        int[] dp = new int[nums.length];\n        dp[dp.length - 1] = nums[dp.length - 1];\n        dp[dp.length - 2] = Math.max(nums[dp.length - 1], nums[dp.length - 2]);\n        for (int i = dp.length - 3; i &gt;= 0; i--) {\n            dp[i] = Math.max(nums[i] + dp[i + 2], dp[i + 1]);\n        }\n        return Math.max(dp[0], dp[1]);\n    }</code></pre>\n<hr /></div>\n<h2>213. 打家劫舍 II</h2>\n<div>&nbsp;</div>\n<div>你是一个专业的小偷，计划偷窃沿街的房屋，每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ，这意味着第一个房屋和最后一个房屋是紧挨着的。同时，相邻的房屋装有相互连通的防盗系统，如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警 。</div>\n<div>&nbsp;</div>\n<div>给定一个代表每个房屋存放金额的非负整数数组，计算你 在不触动警报装置的情况下 ，今晚能够偷窃到的最高金额。</div>\n<div>&nbsp;</div>\n<pre>环状排列意味着第一个房屋和最后一个房屋中最多只能选择一个偷窃，因此可以把此环状排列房间问题约化为两个单排排列房屋子问题。</pre>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>示例 1：</div>\n<div>&nbsp;</div>\n<div>输入：nums = [2,3,2]</div>\n<div>输出：3</div>\n<div>解释：你不能先偷窃 1 号房屋（金额 = 2），然后偷窃 3 号房屋（金额 = 2）, 因为他们是相邻的。</div>\n<div>示例 2：</div>\n<div>&nbsp;</div>\n<div>输入：nums = [1,2,3,1]</div>\n<div>输出：4</div>\n<div>解释：你可以先偷窃 1 号房屋（金额 = 1），然后偷窃 3 号房屋（金额 = 3）。</div>\n<div>&nbsp; &nbsp; &nbsp;偷窃到的最高金额 = 1 + 3 = 4 。</div>\n<div>示例 3：</div>\n<div>&nbsp;</div>\n<div>输入：nums = [0]</div>\n<div>输出：0</div>\n<div>&nbsp;</div>\n<div>\n<pre class=\"language-java\"><code>  public int rob(int[] nums) {\n        if (nums.length == 1) {\n            return nums[0];\n        }\n        int r1 = robRange(Arrays.copyOfRange(nums, 0, nums.length - 1));\n        int r2 = robRange(Arrays.copyOfRange(nums, 1, nums.length));\n        return Math.max(r1, r2);\n    }\n\n    public int robRange(int[] nums) {\n        if (nums.length == 1) {\n            return nums[0];\n        }\n        if (nums.length == 2) {\n            return Math.max(nums[0], nums[1]);\n        }\n        //dp[i]=nums[i]+dp[i+2] or dp[i+1]\n        int[] dp = new int[nums.length];\n        dp[dp.length - 1] = nums[dp.length - 1];\n        dp[dp.length - 2] = Math.max(nums[dp.length - 1], nums[dp.length - 2]);\n        for (int i = dp.length - 3; i &gt;= 0; i--) {\n            dp[i] = Math.max(nums[i] + dp[i + 2], dp[i + 1]);\n        }\n        return Math.max(dp[0], dp[1]);\n    }</code></pre>\n</div>\n<div>&nbsp;</div>', '198. 打家劫舍 213. 打家劫舍 II', '', 0, 1, 1, 84, 2, '2021-12-03 16:33:01', '2021-12-03 16:33:01');
INSERT INTO `t_blog` VALUES (134, '69. Sqrt(x)', '<div>给你一个非负整数 x ，计算并返回 x 的 算术平方根 。</div>\n<div>&nbsp;</div>\n<div>由于返回类型是整数，结果只保留 整数部分 ，小数部分将被 舍去 。</div>\n<div>&nbsp;</div>\n<div>注意：不允许使用任何内置指数函数和算符，例如 pow(x, 0.5) 或者 x ** 0.5 。</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>示例 1：</div>\n<div>&nbsp;</div>\n<div>输入：x = 4</div>\n<div>输出：2</div>\n<div>示例 2：</div>\n<div>&nbsp;</div>\n<div>输入：x = 8</div>\n<div>输出：2</div>\n<div>解释：8 的算术平方根是 2.82842..., 由于返回类型是整数，小数部分将被舍去。</div>\n<div>&nbsp;</div>\n<div>\n<pre class=\"language-java\"><code> public int mySqrt(int x) {\n        int left = 0, right = x;\n        while (left &lt; right) {\n            int mid = (left + right + 1) &gt;&gt; 1;\n            if (mid * mid &lt;= x) {\n                left = mid;\n            } else {\n                right = mid - 1;\n            }\n        }\n        return left;\n    }</code></pre>\n</div>\n<h1>二分查找</h1>\n<p>二分的本质并非&ldquo;单调性&rdquo;，而是&ldquo;边界&rdquo;，只要找到某种性质，使得整个区间一分为二，那么就可以用二分把边界点二分出来。</p>\n<h2><a id=\"user-content-算法模板\" class=\"anchor\" href=\"https://gitee.com/Doocs/leetcode/blob/main/basic/searching/BinarySearch/README.md#%E7%AE%97%E6%B3%95%E6%A8%A1%E6%9D%BF\"></a>算法模板</h2>\n<pre class=\"language-java\"><code>    int search01(int[] nums, int left, int right, int target) {\n        while (left &lt; right) {\n            int mid = (left + right) &gt;&gt; 1;\n            if (nums[mid] &gt;= target) {\n                right = mid;\n            } else {\n                left = mid + 1;\n            }\n        }\n        return left;\n    }\n\n    int search02(int[] nums, int left, int right, int target) {\n        while (left &lt; right) {\n            int mid = (left + right + 1) &gt;&gt; 1;\n            if (nums[mid] &lt;= target) {\n                left = mid;\n            } else {\n                right = mid - 1;\n            }\n        }\n        return left;\n    }</code></pre>', '69. Sqrt(x)', '', 0, 1, 1, 122, 2, '2021-12-03 16:37:20', '2021-12-03 16:37:20');
INSERT INTO `t_blog` VALUES (135, 'ParallelStream导致的ThreadLocal丢失问题', '<p><strong>并行流parallelStream</strong></p>\n<p>parallelStream提供了流的并行处理，它是Stream的另一重要特性，其底层使用Fork/Join框架实现。简单理解就是<a class=\"hl hl-1\" href=\"https://so.csdn.net/so/search?from=pc_blog_highlight&amp;q=%E5%A4%9A%E7%BA%BF%E7%A8%8B\" target=\"_blank\" rel=\"noopener\">多线程</a>异步任务的一种实现。</p>\n<p>&nbsp;</p>\n<p><strong>parallelStream配合ThreadLocal问题复现</strong></p>\n<pre class=\"language-java\"><code>/**\n * @author: peng\n * @date: 2021/12/7\n */\npublic class ParallelStreamTest {\n    static ThreadLocal&lt;String&gt; threadLocal = new ThreadLocal&lt;&gt;();\n\n    public static void main(String[] args) {\n        threadLocal.set(\"主线程的标识-张三\");\n        List&lt;Integer&gt; list1 = new ArrayList&lt;&gt;();\n        for (int i = 0; i &lt; 15; i++) {\n            list1.add(i);\n        }\n\n        System.out.println(list1);\n        System.out.println(\"主线程执行前ThreadLocal:\" + threadLocal.get());\n        System.out.println(\"---------------------------------------------\");\n        List&lt;String&gt; result = list1.stream().parallel().map(ParallelStreamTest.proxy((l1 -&gt;\n                //任务运行依赖threadLocal\n                threadLocal.get() + l1\n        ))).collect(Collectors.toList());\n        System.out.println(\"---------------------------------------------\");\n        System.out.println(result);\n        System.out.println(\"主线程执行后ThreadLocal:\" + threadLocal.get());\n\n\n    }\n\n    public static &lt;T, R&gt; Function&lt;T, R&gt; proxy(Function&lt;T, R&gt; function) {\n        //主线程\n        Thread mainThread = Thread.currentThread();\n        String mainLocal = threadLocal.get();\n        System.out.println(\"主线程:\" + mainThread.getName());\n        System.out.println(\"主线程的ThreadLocal\" + mainLocal);\n        System.out.println(\"---------------------------------------------\");\n        return t -&gt; {\n            //主线程的 threadLocal -&gt; 执行线程的 threadLocal\n            threadLocal.set(mainLocal);\n            //执行线程\n            //System.out.println(\"执行线程:\" + Thread.currentThread().getName() + \"--执行线程的ThreadLocal:\" + threadLocal.get());\n            try {\n                //执行任务xxx(需要用到threadLocal)\n                return function.apply(t);\n            } finally {\n                //清除 执行线程的 threadLocal\n                threadLocal.remove();\n            }\n        };\n    }\n\n}</code></pre>\n<p>在执行任务时 执行线程先获取主线程的threadLocal复制到自己的threadLocal中 任务执行完成后 清除执行线程的thradLocal</p>\n<p>&nbsp;</p>\n<p>执行结果如下:</p>\n<pre class=\"language-markup\"><code>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]\n主线程执行前ThreadLocal:主线程的标识-张三\n---------------------------------------------\n主线程:main\n主线程的ThreadLocal主线程的标识-张三\n---------------------------------------------\n---------------------------------------------\n[主线程的标识-张三0, 主线程的标识-张三1, 主线程的标识-张三2, 主线程的标识-张三3, 主线程的标识-张三4, 主线程的标识-张三5, 主线程的标识-张三6, 主线程的标识-张三7, 主线程的标识-张三8, 主线程的标识-张三9, 主线程的标识-张三10, 主线程的标识-张三11, 主线程的标识-张三12, 主线程的标识-张三13, 主线程的标识-张三14]\n主线程执行后ThreadLocal:null\n\nProcess finished with exit code 0\n</code></pre>\n<pre>在并行流操作执行完成后 主线程的threadLocal 被清除了!<br />加上线程信息输出再看下结果:</pre>\n<pre class=\"language-java\"><code>            //执行线程\n            System.out.println(\"执行线程:\" + Thread.currentThread().getName() + \"--执行线程的ThreadLocal:\" + threadLocal.get());</code></pre>\n<pre class=\"language-java\"><code>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]\n主线程执行前ThreadLocal:主线程的标识-张三\n---------------------------------------------\n主线程:main\n主线程的ThreadLocal主线程的标识-张三\n---------------------------------------------\n执行线程:main--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-4--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-2--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-3--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-5--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-1--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-5--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-7--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-3--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-2--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:main--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-6--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-4--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-5--执行线程的ThreadLocal:主线程的标识-张三\n执行线程:ForkJoinPool.commonPool-worker-1--执行线程的ThreadLocal:主线程的标识-张三\n---------------------------------------------\n[主线程的标识-张三0, 主线程的标识-张三1, 主线程的标识-张三2, 主线程的标识-张三3, 主线程的标识-张三4, 主线程的标识-张三5, 主线程的标识-张三6, 主线程的标识-张三7, 主线程的标识-张三8, 主线程的标识-张三9, 主线程的标识-张三10, 主线程的标识-张三11, 主线程的标识-张三12, 主线程的标识-张三13, 主线程的标识-张三14]\n主线程执行后ThreadLocal:null\n</code></pre>\n<p>main主线程也参与到了任务执行 所以 threadLocal.remove(); 会把主线程的threadLocal清除</p>\n<p><strong>解决方案:threadLocal.remove() 前对线程做判断</strong></p>\n<pre class=\"language-java\"><code>                if (!mainThread.equals(Thread.currentThread())) {\n                    //清除 执行线程的 threadLocal\n                    threadLocal.remove();\n                }</code></pre>\n<p>执行结果:</p>\n<pre class=\"language-java\"><code>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]\n主线程执行前ThreadLocal:主线程的标识-张三\n---------------------------------------------\n主线程:main\n主线程的ThreadLocal主线程的标识-张三\n---------------------------------------------\n---------------------------------------------\n[主线程的标识-张三0, 主线程的标识-张三1, 主线程的标识-张三2, 主线程的标识-张三3, 主线程的标识-张三4, 主线程的标识-张三5, 主线程的标识-张三6, 主线程的标识-张三7, 主线程的标识-张三8, 主线程的标识-张三9, 主线程的标识-张三10, 主线程的标识-张三11, 主线程的标识-张三12, 主线程的标识-张三13, 主线程的标识-张三14]\n主线程执行后ThreadLocal:主线程的标识-张三\n\nProcess finished with exit code 0\n</code></pre>\n<hr />\n<div>\n<div>&nbsp;</div>\n</div>', 'ParallelStream导致的ThreadLocal丢失问题', '', 0, 1, 1, 139, 6, '2021-12-16 18:42:24', '2021-12-16 18:42:24');
INSERT INTO `t_blog` VALUES (136, '从parallelStream认识Fork/Join 框架', '<div>\n<h1>parallelStream 介绍</h1>\n<h2>引言</h2>\n<p>大家应该已经对Stream有过很多的了解，对其原理及常见使用方法已经也有了一定的认识。流在处理数据进行一些迭代操作的时候确认很方便，但是在执行一些耗时或是占用资源很高的任务时候，串行化的流无法带来速度/性能上的提升，并不能满足我们的需要，通常我们会使用多线程来并行或是分片分解执行任务，而在Stream中也提供了这样的并行方法，那就是使用parallelStream()方法或者是使用stream().parallel()来转化为并行流。开箱即用的并行流的使用看起来如此简单，然后我们就可能会忍不住思考，并行流的实现原理是怎样的？它的使用会给我们带来多大的性能提升？我们可以在什么场景下使用以及使用时应该注意些什么？</p>\n<p>首先我们看一下Java 的并行 API 演变历程基本如下：</p>\n<ul>\n<li>1.0-1.4 中的 java.lang.Thread</li>\n<li>5.0 中的 java.util.concurrent</li>\n<li>6.0 中的 Phasers 等</li>\n<li>7.0 中的 Fork/Join 框架</li>\n<li>8.0 中的 Lambda</li>\n</ul>\n<h2>parallelStream是什么？</h2>\n<p>先看一下源码</p>\n<div class=\"_2Uzcx_\"><button class=\"VJbwyy\" type=\"button\" aria-label=\"复制代码\"></button>\n<pre class=\"line-numbers  language-java\"><code>/**\n * Returns a possibly parallel {@code Stream} with this collection as its\n * source.  It is allowable for this method to return a sequential stream.\n *\n * &lt;p&gt;This method should be overridden when the {@link #spliterator()}\n * method cannot return a spliterator that is {@code IMMUTABLE},\n * {@code CONCURRENT}, or &lt;em&gt;late-binding&lt;/em&gt;. (See {@link #spliterator()}\n * for details.)\n *\n * @implSpec\n * The default implementation creates a parallel {@code Stream} from the\n * collection\'s {@code Spliterator}.\n *\n * @return a possibly parallel {@code Stream} over the elements in this\n * collection\n * @since 1.8\n */\ndefault Stream&lt;E&gt; parallelStream() {\n    return StreamSupport.stream(spliterator(), true);\n}\n</code></pre>\n</div>\n<p>注意其中的代码注释的返回值 <code>@return a possibly parallel</code> 一句说明调用了这个方法，只是可能会返回一个并行的流，流是否能并行执行还受到其他一些条件的约束。<br />parallelStream其实就是一个并行执行的流，它通过默认的ForkJoinPool，<strong>可能</strong>提高你的多线程任务的速度。<br />引用<a href=\"https://links.jianshu.com/go?to=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F21163108%2Fcustom-thread-pool-in-java-8-parallel-stream\" target=\"_blank\" rel=\"noopener\">Custom thread pool in Java 8 parallel stream</a>上面的两段话：</p>\n<blockquote>\n<p>The parallel streams use the default <code>ForkJoinPool.commonPool</code> which <a href=\"https://links.jianshu.com/go?to=http%3A%2F%2Fdocs.oracle.com%2Fjavase%2F8%2Fdocs%2Fapi%2Fjava%2Futil%2Fconcurrent%2FForkJoinPool.html\" target=\"_blank\" rel=\"noopener\">by default has one less threads as you have processors</a>, as returned by <code>Runtime.getRuntime().availableProcessors()</code> (This means that parallel streams use all your processors because they also use the main thread)。</p>\n</blockquote>\n<p>做个实验来证明上面这句话的真实性：</p>\n<div class=\"_2Uzcx_\"><button class=\"VJbwyy\" type=\"button\" aria-label=\"复制代码\"></button>\n<pre class=\"line-numbers  language-cpp\"><code>public static void main(String[] args) {\n    IntStream list = IntStream.range(0, 10);\n    Set&lt;Thread&gt; threadSet = new CopyOnWriteArraySet&lt;&gt;();\n    //开始并行执行\n    list.parallel().forEach(i -&gt; {\n        Thread thread = Thread.currentThread();\n        System.err.println(\"integer：\" + i + \"，\" + \"currentThread:\" + thread.getName());\n        threadSet.add(thread);\n    });\n    System.out.println(\"all threads：\" + threadSet.stream().map(Thread::getName).collect(Collectors.joining(\":\")));\n}\n</code></pre>\n</div>\n<div class=\"image-package\">\n<div class=\"image-container\">\n<div class=\"image-container-fill\">&nbsp;</div>\n<div class=\"image-view\" data-width=\"699\" data-height=\"339\"><img class=\"\" src=\"//upload-images.jianshu.io/upload_images/13932958-263c866e35df81e5.png?imageMogr2/auto-orient/strip|imageView2/2/w/699/format/webp\" data-original-src=\"//upload-images.jianshu.io/upload_images/13932958-263c866e35df81e5.png\" data-original-width=\"699\" data-original-height=\"339\" data-original-format=\"image/png\" data-original-filesize=\"32709\" data-image-index=\"0\" /></div>\n</div>\n<div class=\"image-caption\">运行结果</div>\n</div>\n<br />\n<p>从运行结果里面我们可以很清楚的看到parallelStream同时使用了主线程和ForkJoinPool.commonPool创建的线程。<br />值得说明的是这个运行结果并不是唯一的，实际运行的时候可能会得到多个结果，比如：</p>\n<br />\n<div class=\"image-package\">\n<div class=\"image-container\">\n<div class=\"image-container-fill\">&nbsp;</div>\n<div class=\"image-view\" data-width=\"1136\" data-height=\"341\"><img class=\"\" src=\"//upload-images.jianshu.io/upload_images/13932958-e1836ce1a66f41ec.png?imageMogr2/auto-orient/strip|imageView2/2/w/1136/format/webp\" data-original-src=\"//upload-images.jianshu.io/upload_images/13932958-e1836ce1a66f41ec.png\" data-original-width=\"1136\" data-original-height=\"341\" data-original-format=\"image/png\" data-original-filesize=\"44847\" data-image-index=\"1\" /></div>\n</div>\n<div class=\"image-caption\">结果</div>\n</div>\n<br />\n<p>甚至你的运行结果里面只有main线程。</p>\n<p>来源于java 8 实战的书籍的一段话：</p>\n<blockquote>\n<p>并行流内部使用了默认的<code>ForkJoinPool</code>（7.2节会进一步讲到分支/合并框架），它默认的线程数量就是你的处理器数量，这个值是由<code>Runtime.getRuntime().available- Processors()</code>得到的。 但是你可以通过系统属性<code>java.util.concurrent.ForkJoinPool.common. parallelism</code>来改变线程池大小，如下所示： <code>System.setProperty(\"java.util.concurrent.ForkJoinPool.common.parallelism\",\"12\");</code> 这是一个全局设置，因此它将影响代码中所有的并行流。反过来说，目前还无法专为某个 并行流指定这个值。一般而言，让<code>ForkJoinPool</code>的大小等于处理器数量是个不错的默认值， 除非你有很好的理由，否则我们强烈建议你不要修改它。</p>\n</blockquote>\n<div class=\"_2Uzcx_\"><button class=\"VJbwyy\" type=\"button\" aria-label=\"复制代码\"></button>\n<pre class=\"line-numbers  language-csharp\"><code>// 设置全局并行流并发线程数\nSystem.setProperty(\"java.util.concurrent.ForkJoinPool.common.parallelism\", \"12\");\nSystem.out.println(ForkJoinPool.getCommonPoolParallelism());// 输出 12\nSystem.setProperty(\"java.util.concurrent.ForkJoinPool.common.parallelism\", \"20\");\nSystem.out.println(ForkJoinPool.getCommonPoolParallelism());// 输出 12\n</code></pre>\n</div>\n<p>为什么两次的运行结果是一样的呢？上面刚刚说过了这是一个全局设置，<code>java.util.concurrent.ForkJoinPool.common.parallelism</code>是final类型的，整个JVM中只允许设置一次。既然默认的并发线程数不能反复修改，那怎么进行不同线程数量的并发测试呢？答案是：引入ForkJoinPool</p>\n<div class=\"_2Uzcx_\"><button class=\"VJbwyy\" type=\"button\" aria-label=\"复制代码\"></button>\n<pre class=\"line-numbers  language-go\"><code>IntStream range = IntStream.range(1, 100000);\n// 传入parallelism\nnew ForkJoinPool(parallelism).submit(() -&gt; range.parallel().forEach(System.out::println)).get();\n</code></pre>\n</div>\n<p>因此，使用parallelStream时需要注意的一点是，多个parallelStream之间默认使用的是同一个线程池，所以IO操作尽量不要放进parallelStream中，否则会阻塞其他parallelStream。</p>\n<blockquote>\n<p>Using a ForkJoinPool and submit for a parallel stream does not reliably use all threads. If you look at this ( <a href=\"https://links.jianshu.com/go?to=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F28985704%2Fparallel-stream-from-a-hashset-doesnt-run-in-parallel\" target=\"_blank\" rel=\"noopener\">Parallel stream from a HashSet doesn\'t run in parallel</a> ) and this ( <a href=\"https://links.jianshu.com/go?to=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F36947336%2Fwhy-does-the-parallel-stream-not-use-all-the-threads-of-the-forkjoinpool\" target=\"_blank\" rel=\"noopener\">Why does the parallel stream not use all the threads of the ForkJoinPool?</a> ), you\'ll see the reasoning.</p>\n</blockquote>\n<div class=\"_2Uzcx_\"><button class=\"VJbwyy\" type=\"button\" aria-label=\"复制代码\"></button>\n<pre class=\"line-numbers  language-csharp\"><code>// 获取当前机器CPU处理器的数量\nSystem.out.println(Runtime.getRuntime().availableProcessors());// 输出 4\n// parallelStream默认的并发线程数\nSystem.out.println(ForkJoinPool.getCommonPoolParallelism());// 输出 3\n</code></pre>\n</div>\n<p>为什么parallelStream默认的并发线程数要比CPU处理器的数量少1个？文章的开始已经提过了。因为最优的策略是每个CPU处理器分配一个线程，然而主线程也算一个线程，所以要占一个名额。<br />这一点可以从源码中看出来：</p>\n<div class=\"_2Uzcx_\"><button class=\"VJbwyy\" type=\"button\" aria-label=\"复制代码\"></button>\n<pre class=\"line-numbers  language-java\"><code>static final int MAX_CAP      = 0x7fff;        // max #workers - 1\n// 无参构造函数\npublic ForkJoinPool() {\n        this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),\n             defaultForkJoinWorkerThreadFactory, null, false);\n}\n</code></pre>\n</div>\n<h2>从parallelStream认识<a href=\"https://links.jianshu.com/go?to=https%3A%2F%2Fwww.infoq.cn%2Farticle%2Ffork-join-introduction%2F\" target=\"_blank\" rel=\"noopener\">Fork/Join 框架</a></h2>\n<p>Fork/Join 框架的核心是采用分治法的思想，将一个大任务拆分为若干互不依赖的子任务，把这些子任务分别放到不同的队列里，并为每个队列创建一个单独的线程来执行队列里的任务。同时，为了最大限度地提高并行处理能力，采用了工作窃取算法来运行任务，也就是说当某个线程处理完自己工作队列中的任务后，尝试当其他线程的工作队列中窃取一个任务来执行，直到所有任务处理完毕。所以为了减少线程之间的竞争，通常会使用双端队列，被窃取任务线程永远从双端队列的头部拿任务执行，而窃取任务的线程永远从双端队列的尾部拿任务执行。</p>\n<ul>\n<li>\n<p>Fork/Join 的运行流程图</p>\n<br />\n<div class=\"image-package\">\n<div class=\"image-container\">\n<div class=\"image-container-fill\">&nbsp;</div>\n<div class=\"image-view\" data-width=\"627\" data-height=\"625\"><img class=\"\" src=\"//upload-images.jianshu.io/upload_images/13932958-dbceae46ea7c15c3.png?imageMogr2/auto-orient/strip|imageView2/2/w/627/format/webp\" data-original-src=\"//upload-images.jianshu.io/upload_images/13932958-dbceae46ea7c15c3.png\" data-original-width=\"627\" data-original-height=\"625\" data-original-format=\"image/png\" data-original-filesize=\"119518\" data-image-index=\"2\" /></div>\n</div>\n<div class=\"image-caption\">image.png</div>\n</div>\n<br />\n<p>简单地说就是大任务拆分成小任务，分别用不同线程去完成，然后把结果合并后返回。所以第一步是拆分，第二步是分开运算，第三步是合并。这三个步骤分别对应的就是Collector的supplier,accumulator和combiner。</p>\n</li>\n<li>\n<p>工作窃取算法<br />forkjoin最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的cpu,那么如何利用好这个空闲的cpu就成了提高性能的关键,而这里我们要提到的工作窃取（work-stealing）算法就是整个forkjion框架的核心理念,工作窃取（work-stealing）算法是指某个线程从其他队列里窃取任务来执行。</p>\n<br />\n<div class=\"image-package\">\n<div class=\"image-container\">\n<div class=\"image-container-fill\">&nbsp;</div>\n<div class=\"image-view\" data-width=\"414\" data-height=\"358\"><img class=\"\" src=\"//upload-images.jianshu.io/upload_images/13932958-ffe0d5ddd7101bbc.png?imageMogr2/auto-orient/strip|imageView2/2/w/414/format/webp\" data-original-src=\"//upload-images.jianshu.io/upload_images/13932958-ffe0d5ddd7101bbc.png\" data-original-width=\"414\" data-original-height=\"358\" data-original-format=\"image/png\" data-original-filesize=\"69004\" data-image-index=\"3\" /></div>\n</div>\n<div class=\"image-caption\">image.png</div>\n</div>\n</li>\n</ul>\n<h2>使用parallelStream的利弊</h2>\n<p>使用parallelStream的几个好处：</p>\n<ol>\n<li>代码优雅，可以使用lambda表达式，原本几句代码现在一句可以搞定(相比其他实现并行的方式)；</li>\n<li>运用多核特性(forkAndJoin)并行处理，大幅提高效率。</li>\n</ol>\n<p>然而，任何事物都不是完美的，并行流也不例外，其中最明显的就是使用(parallel)Stream极其不便于代码的跟踪调试，此外并行流带来的不确定性也使得我们对它的使用变得格外谨慎。我们得去了解更多的并行流的相关知识来保证自己能够正确的使用这把双刃剑。</p>\n<p>parallelStream使用时需要注意的点：</p>\n<ol>\n<li><strong>parallelStream是线程不安全的；</strong></li>\n</ol>\n<div class=\"_2Uzcx_\"><button class=\"VJbwyy\" type=\"button\" aria-label=\"复制代码\"></button>\n<pre class=\"line-numbers  language-php\"><code>List&lt;Integer&gt; values = new ArrayList&lt;&gt;();\nIntStream.range(1, 10000).parallel().forEach(values::add);\nSystem.out.println(values.size());\n</code></pre>\n</div>\n<p>values集合大小可能不是10000。集合里面可能会存在null元素或者抛出下标越界的异常信息。<br />原因：ArrayList不是线程安全的集合，add方法在多线程环境下会存在并发问题。<br />执行add方法时，会先将此容器的大小增加。。即size++，然后将传进的元素赋值给新增的elementData[size++]，即新的内存空间。但是此时如果在size++后直接来取这个List,而没有让add完成赋值操作，则会导致此List的长度加一，，但是最后一个元素是空（null），所以在获取它进行计算的时候报了空指针异常。而下标越界还不能仅仅依靠这个来解释，如果你观察发生越界时的数组下标，分别为10、15、22、33、49和73。结合前面讲的数组自动机制，数组初始长度为10，第一次扩容为15=10+10/2，第二次扩容22=15+15/2，第三次扩容33=22+22/2...以此类推，我们不难发现，越界异常都发生在数组扩容之时。<br /><code>grow()</code>方法解释了基于数组的ArrayList是如何扩容的。数组进行扩容时，会将老数组中的元素重新拷贝一份到新的数组中，通过<code>oldCapacity + (oldCapacity &gt;&gt; 1)</code>运算，每次数组容量的增长大约是其原容量的1.5倍。</p>\n<div class=\"_2Uzcx_\"><button class=\"VJbwyy\" type=\"button\" aria-label=\"复制代码\"></button>\n<pre class=\"line-numbers  language-java\"><code>   /**\n    * Increases the capacity to ensure that it can hold at least the\n    * number of elements specified by the minimum capacity argument.\n    *\n    * @param minCapacity the desired minimum capacity\n    */\n   private void grow(int minCapacity) {\n       // overflow-conscious code\n       int oldCapacity = elementData.length;\n       int newCapacity = oldCapacity + (oldCapacity &gt;&gt; 1);// 1.5倍扩容\n       if (newCapacity - minCapacity &lt; 0)\n           newCapacity = minCapacity;\n       if (newCapacity - MAX_ARRAY_SIZE &gt; 0)\n           newCapacity = hugeCapacity(minCapacity);\n       // minCapacity is usually close to size, so this is a win:\n       elementData = Arrays.copyOf(elementData, newCapacity);// 拷贝旧的数组到新的数组中\n   }\n\n\n   /**\n    * Appends the specified element to the end of this list.\n    *\n    * @param e element to be appended to this list\n    * @return &lt;tt&gt;true&lt;/tt&gt; (as specified by {@link Collection#add})\n    */\n   public boolean add(E e) {\n       ensureCapacityInternal(size + 1);  // Increments modCount!! 检查array容量\n       elementData[size++] = e;// 赋值，增大Size的值\n       return true;\n   }\n</code></pre>\n</div>\n<p>解决方法：<br />加锁、使用线程安全的集合或者采用<code>collect()</code>或者<code>reduce()</code>操作就是满足线程安全的了。</p>\n<div class=\"_2Uzcx_\"><button class=\"VJbwyy\" type=\"button\" aria-label=\"复制代码\"></button>\n<pre class=\"line-numbers  language-csharp\"><code>List&lt;Integer&gt; values = new ArrayList&lt;&gt;();\nfor (int i = 0; i &lt; 10000; i++) {\n    values.add(i);\n}\nList&lt;Integer&gt; collect = values.stream().parallel().collect(Collectors.toList());\nSystem.out.println(collect.size());\n</code></pre>\n</div>\n<p>再来看下下面的例子，会输出什么呢？</p>\n<div class=\"_2Uzcx_\"><button class=\"VJbwyy\" type=\"button\" aria-label=\"复制代码\"></button>\n<pre class=\"line-numbers  language-java\"><code>Stream&lt;String&gt; stream = Stream.of(\"How\", \"do\", \"you\", \"do\");\nList&lt;String&gt; list = stream.collect(ArrayList::new, ArrayList::add, (t, u) -&gt; {\n    System.out.println(\"t:\" + t + \" u:\" + u);\n    t.addAll(u);\n});\nSystem.out.println(list);\n</code></pre>\n</div>\n<p>为什么collect的第三个参数并没有执行？怎样才会执行到呢？<br />先看一下该方法的相关代码<code>&lt;R&gt; R collect(Supplier&lt;R&gt; supplier, BiConsumer&lt;R, ? super T&gt; accumulator, BiConsumer&lt;R, R&gt; combiner)</code>，再集合一下前面讲到的Fork/Join的运行流程：第一步是拆分，第二步是分开运算，第三步是合并。这三个步骤是不是刚好分别对应的就是Collector的supplier,accumulator和combiner呢？当流转为并行流的时候，会对原始流做切分，分别执行中间的过程，最后合并结果，这个时候就会执行到第三个方法了。</p>\n<ol start=\"2\">\n<li>parallelStream 适用的场景是CPU密集型的，只是做到别浪费CPU，假如本身电脑CPU的负载很大，那还到处用并行流，那并不能起到作用；</li>\n</ol>\n<ul>\n<li>I/O密集型 磁盘I/O、网络I/O都属于I/O操作，这部分操作是较少消耗CPU资源，一般并行流中不适用于I/O密集型的操作，就比如使用并流行进行大批量的消息推送，涉及到了大量I/O，使用并行流反而慢了很多</li>\n<li>CPU密集型 计算类型就属于CPU密集型了，这种操作并行流就能提高运行效率。</li>\n</ul>\n<ol start=\"3\">\n<li>不要在多线程中使用parallelStream，原因同上类似，大家都抢着CPU是没有提升效果，反而还会加大线程切换开销；</li>\n<li>会带来不确定性，请确保每条处理无状态且没有关联；</li>\n<li>考虑NQ模型：N可用的数据量，Q针对每个数据元素执行的计算量，乘积 N*Q 越大，就越有可能获得并行提速。当N * Q &gt; 10000（大概是集合大小1000以上）就会获得有效提升；</li>\n<li>parallelStream是创建一个并行的Stream,而且它的并行操作是不具备线程传播性的,所以是无法获取ThreadLocal创建的线程变量的值；</li>\n<li><strong>在使用并行流的时候是无法保证元素的顺序的，也就是即使你用了同步集合也只能保证元素都正确但无法保证其中的顺序</strong>；</li>\n<li>lambda的执行并不是瞬间完成的，所有使用parallel stream的程序都有可能成为阻塞程序的源头，并且在执行过程中程序中的其他部分将无法访问这些workers，这意味着任何依赖parallel streams的程序在什么别的东西占用着common ForkJoinPool时将会变得不可预知并且暗藏危机。</li>\n</ol>\n<hr />\n<p>最后补充一下并行和并发的关系<br />并行是同时发生的多个并发事件，在同一时刻利用CPU的多核，让多个线程运行在多个CPU上，因此并行比并发具有更高的CPU利用率，效率相对更高，并行具有并发的含义，但并发不一定并行，并发事件之间不一定要同一时刻发生。</p>\n</div>\n<p><a href=\"https://www.jianshu.com/p/3d4e76467990\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '从parallelStream认识Fork/Join 框架', '', 0, 1, 1, 131, 6, '2021-12-16 19:12:00', '2021-12-16 19:12:00');
INSERT INTO `t_blog` VALUES (137, 'Canal实现MySql数据监听', '<h2>一、什么是canal</h2>\n<div>我们先看官网的介绍</div>\n<div>&nbsp;</div>\n<div>canal，译意为水道/管道/沟渠，主要用途是基于 MySQL 数据库增量日志解析，提供增量数据订阅和消费。</div>\n<div>&nbsp;</div>\n<div>这句介绍有几个关键字：增量日志，增量数据订阅和消费。</div>\n<div>&nbsp;</div>\n<div>这里我们可以简单地把canal理解为一个用来同步增量数据的一个工具。</div>\n<div>&nbsp;</div>\n<div>接下来我们看一张官网提供的示意图：</div>\n<div>&nbsp;</div>\n<div><img src=\"https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zdGF0aWMubG92ZWJpbGliaWxpLmNvbS9waWMvY2FuYWxfc3l0LnBuZw?x-oss-process=image/format,png\" /></div>\n<div>&nbsp;</div>\n<div>canal的工作原理就是把自己伪装成MySQL slave，模拟MySQL slave的交互协议向MySQL Mater发送 dump协议，MySQL mater收到canal发送过来的dump请求，开始推送binary log给canal，然后canal解析binary log，再发送到存储目的地，比如MySQL，Kafka，Elastic Search等等。</div>\n<div>&nbsp;</div>\n<h2>二、canal能做什么</h2>\n<div>以下参考canal官网。</div>\n<div>&nbsp;</div>\n<div>与其问canal能做什么，不如说数据同步有什么作用。</div>\n<div>&nbsp;</div>\n<div>但是canal的数据同步不是全量的，而是增量。基于binary log增量订阅和消费，canal可以做：</div>\n<div>&nbsp;</div>\n<div>数据库镜像</div>\n<div>数据库实时备份</div>\n<div>索引构建和实时维护</div>\n<div>业务cache(缓存)刷新</div>\n<div>带业务逻辑的增量数据处理</div>\n<h2>三、如何搭建canal</h2>\n<div>3.1 首先有一个MySQL服务器</div>\n<div>当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x</div>\n<div>&nbsp;</div>\n<div>我的Linux服务器安装的MySQL服务器是5.7版本。</div>\n<div>&nbsp;</div>\n<div>MySQL的安装这里就不演示了，比较简单，网上也有很多教程。</div>\n<div>&nbsp;</div>\n<div>\n<p dir=\"auto\">mysql配置：</p>\n<p dir=\"auto\">1.编辑mysql配置文件 $ sudo vim /etc/my.cnf</p>\n<div class=\"snippet-clipboard-content position-relative overflow-auto\">\n<pre><code></code></pre>\n<pre class=\"language-markup\"><code>	[mysqld]  \n	log-bin=mysql-bin #binlog文件名（也可以使用绝对路径）\n	binlog-format=ROW #选择row模式  \n	server_id=1 	  #实例唯一ID，不能和canal的slaveId重复\n\n保存并退出，并重启mysql\n	$ sudo service mysql restart</code></pre>\n<pre><code>改了配置文件之后，重启MySQL，使用命令查看是否打开binlog模式：<br /><img src=\"https://img-blog.csdnimg.cn/20200808151606261.png#pic_center\" alt=\"在这里插入图片描述\" /><br /></code></pre>\n</div>\n</div>\n<div>\n<p dir=\"auto\">2.创建 mysql账号密码（账号密码自定 权限自定）&nbsp;</p>\n<pre class=\"language-markup\"><code>-- CREATE USER canal IDENTIFIED BY \'password\'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO \'canal\'@\'%\'; \n-- GRANT ALL PRIVILEGES ON *.* TO \'canal\'@\'%\' ; FLUSH PRIVILEGES;</code></pre>\n</div>\n<div>&nbsp;</div>\n<div>查看binlog日志文件列表：</div>\n<div>&nbsp;</div>\n<div>查看当前正在写入的binlog文件：</div>\n<div>&nbsp;</div>\n<div>MySQL服务器这边就搞定了，很简单。</div>\n<div><hr /></div>\n<div>3.2 RabbitMQ</div>\n<div>\n<p>RabbitMQ安装也没啥好说的，教程一大堆。</p>\n<p>安装完成后使用默认guest登录，新建一个Exchange和Queues，并将它们绑定到一起去。</p>\n<ul>\n<li>新建Exchange</li>\n</ul>\n<p><img src=\"https://gitee.com/gerlos/pictures/raw/master/null/20200907192749.png\" alt=\"image-20200907192747481\" /></p>\n<ul>\n<li>\n<p>新建Queues</p>\n<p>同时新建路由键routing key备用。</p>\n</li>\n<li>\n<p>创建连接MQ的账号密码，同时顺手一起创建一个消费者账号<strong>canalConsumer</strong>用于SpringCloud项目中连接MQ所用。</p>\n</li>\n</ul>\n<hr />\n<p>3.3 Canal Server配置</p>\n<pre><code>canal server 模拟mysql从库并向mysql发送dump命令获取mysql binlog数据。\n\n1.下载解压项目\n可从阿里项目下载最新版本 deployer ：\n[https://github.com/alibaba/canal/releases](https://github.com/alibaba/canal/releases)\n\n2.配置项目：\n	# 公共配置\n	$ sudo vim conf/canal.properties\n		\n		canal.port= 11111 # canal server 运行端口，保证该端口为占用状态，或者使用其他未占用端口\n	\n	保存退出。\n	\n	# 实例配置\n	$ sudo vim conf/example/instance.properties\n		\n		# position info\n		canal.instance.master.address = 127.0.0.1:3306  # mysql连接\n		\n		canal.instance.dbUsername = canal  		# mysql账号\n		canal.instance.dbPassword = canal		# 密码\n		canal.instance.defaultDatabaseName = test	# 需要同步的库名\n		canal.instance.connectionCharset = UTF-8	# mysql编码\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;#模式设置为rabbitMq模式\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;canal.serverMode = rabbitMQ\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;##################################################\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;######### 		    RabbitMQ	     #############\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;##################################################\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;#地址，不需要端口\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;rabbitmq.host = 127.0.0.1\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;#当前Vhost\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;rabbitmq.virtual.host = /\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;#刚才配置的交换机\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;rabbitmq.exchange = cannal-exchange\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;#刚才配置的账号密码\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;rabbitmq.username = cannal\n&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;rabbitmq.password = cannal\n	\n	保存退出。\n	\n3.启动：\n	$ sh bin/startup.sh\n	\n日志文件： $ less logs/canal/canal.log	 # canal server端运行日志\n	  $ less logs/example/example.log   # canal client端连接日志\n	  $ logs/example/meta.log 	    # 实例binlog 读取记录文件（记录变更位置，默认为新增变更(tail)）</code></pre>\n<hr />\n<p>3.4 监听MQ获取消息</p>\n<p>启动springBoot项目 监听MQ并打印</p>\n<p>application.yml</p>\n<pre class=\"language-markup\"><code>  rabbitmq:\n    host: ip地址\n    port: 5672\n    username: admin\n    password: admin</code></pre>\n<p>pom.xml</p>\n<pre class=\"language-markup\"><code>        &lt;!--rabbitmq--&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-amqp&lt;/artifactId&gt;\n        &lt;/dependency&gt;</code></pre>\n<pre class=\"language-java\"><code>package com.peng.rabbitmq;\n\nimport org.springframework.amqp.rabbit.annotation.RabbitHandler;\nimport org.springframework.amqp.rabbit.annotation.RabbitListener;\nimport org.springframework.stereotype.Component;\n\n/**\n * @author peng\n * @date 2021/12/23 15:35\n */\n\n@Component\npublic class DirectReceiver {\n\n    @RabbitHandler\n    @RabbitListener(queues = \"canal.queue\")\n    public void process(byte[] b) {\n        System.out.println(\"DirectReceiver消费者收到消息  : \" + new String(b));\n    }\n\n}</code></pre>\n<p>启动项目，修改数据库字段，控制台输出:</p>\n<pre class=\"language-markup\"><code>DirectReceiver消费者收到消息  : {\"data\":[{\"invoice_id\":\"19\",\"invoice_title_type\":\"0\",\"invoice_type\":\"1\",\"name\":\"\",\"company_name\":\"bb5\",\"status\":\"1\",\"tax_register_number\":\"\",\"invoice_code\":\"\",\"invoice_amount\":\"0\",\"register_address\":\"\",\"register_phone\":\"\",\"bank_name\":\"\",\"bank_no\":\"\",\"contact_name\":\"\",\"contact_phone\":\"\",\"contact_address\":\"\",\"contact_email\":\"\",\"express_no\":\"\",\"express_name\":\"\",\"file_url\":\"\",\"file_path\":\"\",\"supplier_id\":\"0\",\"owner_id\":\"10\",\"user_id\":\"0\",\"remark\":\"\",\"yn\":\"0\",\"create_time\":\"2021-10-21 14:33:29\",\"update_time\":\"2021-12-23 14:08:40\",\"create_pin\":\"\",\"update_pin\":\"\"}],\"database\":\"my_test\",\"es\":1640239720000,\"id\":38,\"isDdl\":false,\"mysqlType\":{\"invoice_id\":\"bigint(10) unsigned\",\"invoice_title_type\":\"tinyint(3)\",\"invoice_type\":\"tinyint(3)\",\"name\":\"varchar(20)\",\"company_name\":\"varchar(50)\",\"status\":\"tinyint(3)\",\"tax_register_number\":\"varchar(20)\",\"invoice_code\":\"varchar(20)\",\"invoice_amount\":\"bigint(20)\",\"register_address\":\"varchar(50)\",\"register_phone\":\"varchar(50)\",\"bank_name\":\"varchar(50)\",\"bank_no\":\"varchar(50)\",\"contact_name\":\"varchar(50)\",\"contact_phone\":\"varchar(50)\",\"contact_address\":\"varchar(255)\",\"contact_email\":\"varchar(50)\",\"express_no\":\"varchar(30)\",\"express_name\":\"varchar(30)\",\"file_url\":\"varchar(255)\",\"file_path\":\"varchar(255)\",\"supplier_id\":\"int(10)\",\"owner_id\":\"int(10)\",\"user_id\":\"int(10)\",\"remark\":\"varchar(255)\",\"yn\":\"tinyint(1)\",\"create_time\":\"timestamp\",\"update_time\":\"timestamp\",\"create_pin\":\"varchar(50)\",\"update_pin\":\"varchar(50)\"},\"old\":[{\"invoice_type\":\"0\",\"update_time\":\"2021-12-23 14:03:03\"}],\"pkNames\":[\"invoice_id\"],\"sql\":\"\",\"sqlType\":{\"invoice_id\":-5,\"invoice_title_type\":-6,\"invoice_type\":-6,\"name\":12,\"company_name\":12,\"status\":-6,\"tax_register_number\":12,\"invoice_code\":12,\"invoice_amount\":-5,\"register_address\":12,\"register_phone\":12,\"bank_name\":12,\"bank_no\":12,\"contact_name\":12,\"contact_phone\":12,\"contact_address\":12,\"contact_email\":12,\"express_no\":12,\"express_name\":12,\"file_url\":12,\"file_path\":12,\"supplier_id\":4,\"owner_id\":4,\"user_id\":4,\"remark\":12,\"yn\":-6,\"create_time\":93,\"update_time\":93,\"create_pin\":12,\"update_pin\":12},\"table\":\"trade_invoice\",\"ts\":1640239720097,\"type\":\"UPDATE\"}\nDirectReceiver消费者收到消息  : {\"data\":[{\"log_id\":\"86329\",\"url\":\"/peng/admin/blog/add\",\"ip_address\":\"223.72.80.105\",\"class_method\":\"com.peng.controller.Admin.BlogController.add\",\"args\":\"[Blog(blId=null, title=Canal实现MySql数据监听, content=&lt;div&gt;一、什么是canal&lt;/div&gt;\\n&lt;div&gt;我们先看官网的介绍&lt;/div&gt;\\n&lt;div&gt;&amp;nbsp;&lt;/div&gt;\\n&lt;div&gt;canal，译意为水道/管道/沟渠，主要用途是基于 MySQL 数据库增量日志解析，提供增量数据订阅和消费。&lt;/div&gt;\\n&lt;div&gt;&amp;nbsp;&lt;/div&gt;\\n&lt;div&gt;这句介绍有几个关键字：增量日志，增量数据订阅和消费。&lt;/div&gt;\\n&lt;div&gt;&amp;nbsp;&lt;/......\",\"create_time\":\"2021-12-23 14:17:17\"}],\"database\":\"My_Blog_db\",\"es\":1640240237000,\"id\":39,\"isDdl\":false,\"mysqlType\":{\"log_id\":\"bigint(11)\",\"url\":\"varchar(255)\",\"ip_address\":\"varchar(32)\",\"class_method\":\"varchar(255)\",\"args\":\"varchar(255)\",\"create_time\":\"timestamp\"},\"old\":null,\"pkNames\":[\"log_id\"],\"sql\":\"\",\"sqlType\":{\"log_id\":-5,\"url\":12,\"ip_address\":12,\"class_method\":12,\"args\":12,\"create_time\":93},\"table\":\"t_request_log\",\"ts\":1640240237450,\"type\":\"INSERT\"}\n</code></pre>\n<p>数据中有数据库名database，表名table，操作类型type，时间等等关键信息，拥有了这些关键信息，那么对于数据我们自己想怎么玩就怎么玩了</p>\n<p>参考:<a href=\"https://blog.csdn.net/u011232863/article/details/108466382\" target=\"_blank\" rel=\"noopener\">mysql+canal+rabbitMq+SpringCloud 实现数据库数据同步监听</a></p>\n<p>&nbsp;</p>\n</div>', 'Canal实现MySql数据监听', '', 0, 1, 1, 151, 7, '2021-12-23 14:17:17', '2021-12-23 14:17:17');
INSERT INTO `t_blog` VALUES (138, 'Mybatis使用模糊查询导致SQL注入问题排查', '<h2>Mybatis #{}和${}</h2>\n<p>我们经常使用的是#{},一般解说是因为这种方式可以防止SQL注入，简单的说#{}这种方式SQL语句是经过预编译的，它是把#{}中间的参数转义成字符串,</p>\n<p>一般能用#的就别用$</p>\n<p>&nbsp;</p>\n<h2>${}SQL注入复现</h2>\n<p>实际开发时有时需要用like做模糊匹配,我这里图省事直接用 ${}拼接 如图:</p>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/012c19ad6ced47f49d23bb44b118cff6.png\" /></p>\n<p>下面我们测试下SQL注入场景,这个sql执行是通过前端搜索框触发的,我们本地启服务打个断点:</p>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/26dec4bb30ab477ba630c73a5dc4a92a.png\" /></p>\n<p>&nbsp;</p>\n<pre class=\"language-markup\"><code>2021-12-29 18:31:57.870 DEBUG 9668 --- [io-8081-exec-29] c.p.m.BlogMapper.findIndexPage_COUNT     : ==&gt;  Preparing: select count(0) from (select bl_id,title,outline,background_image,recommend,commentabled,published,views,ty_id,create_time,update_time from t_blog where published=true AND title like \"%canal%\" order by create_time desc) tmp_count </code></pre>\n<p>看控制台输出的SQL因为用到了 PageHelper分页 会先执行一个 count() 语句,现在我们把参数改下:</p>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/bfc6e19c5e15481cb93bf1f37ff3eb93.png\" /></p>\n<p>再看数据库:</p>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/12592895b91e4a4e9170a8087e1558f4.png\" /></p>\n<p>Person表被创建了,SQL注入成功</p>\n<p>因为是GET请求,我们将</p>\n<pre class=\"language-markup\"><code>%a\")tmp_count;CREATE TABLE Persons ( Id_P int, LastName varchar(255));--</code></pre>\n<p>做URL编码后如下:</p>\n<pre class=\"language-markup\"><code>%25a%22)tmp_count%3BCREATE%20TABLE%20Persons%20(%20Id_P%20int%2C%20LastName%20varchar(255))%3B--</code></pre>\n<p><img class=\"wscnph\" src=\"http://sjpeng.top/779eaf3dc3eb4d18b7173670869fe638.png\" /></p>\n<p>我们删除Person表,将上面语句直接放到页面输入,查看执行结果,Person表又被创建了,SQL注入成功</p>\n<h2>解决方案</h2>\n<p>1.使用#{}配合mysql的concat函数</p>\n<p>2.参数拼接%后再使用#{}</p>\n<p>&nbsp;</p>', 'Mybatis使用模糊查询导致SQL注入问题排查', '', 0, 1, 1, 144, 4, '2021-12-29 18:21:57', '2021-12-29 18:21:57');
INSERT INTO `t_blog` VALUES (139, '55. 跳跃游戏 45. 跳跃游戏 II', '<h2>55. 跳跃游戏</h2>\n<div>给定一个非负整数数组 nums ，你最初位于数组的 第一个下标 。</div>\n<div>&nbsp;</div>\n<div>数组中的每个元素代表你在该位置可以跳跃的最大长度。</div>\n<div>&nbsp;</div>\n<div>判断你是否能够到达最后一个下标。</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>示例 1：</div>\n<div>&nbsp;</div>\n<div>输入：nums = [2,3,1,1,4]</div>\n<div>输出：true</div>\n<div>解释：可以先跳 1 步，从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。</div>\n<div>示例 2：</div>\n<div>&nbsp;</div>\n<div>输入：nums = [3,2,1,0,4]</div>\n<div>输出：false</div>\n<div>解释：无论怎样，总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 ， 所以永远不可能到达最后一个下标。</div>\n<h3>双指针</h3>\n<div>\n<pre class=\"language-java\"><code> public boolean canJump(int[] nums) {\n        int endPoint = nums.length - 1;\n        int left = 0, right = 0 + nums[left];\n        while (right &lt; endPoint) {//right&gt;=终点返回true\n            if (left == right) {\n                return false;\n            }\n            ++left;//left尽量将right右移\n            right = Math.max(right, left + nums[left]);\n        }\n        return true;\n    }</code></pre>\n</div>\n<hr />\n<h2>45. 跳跃游戏 II</h2>\n<div>给你一个非负整数数组 nums ，你最初位于数组的第一个位置。</div>\n<div>&nbsp;</div>\n<div>数组中的每个元素代表你在该位置可以跳跃的最大长度。</div>\n<div>&nbsp;</div>\n<div>你的目标是使用最少的跳跃次数到达数组的最后一个位置。</div>\n<div>&nbsp;</div>\n<div>假设你总是可以到达数组的最后一个位置。</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>示例 1:</div>\n<div>&nbsp;</div>\n<div>输入: nums = [2,3,1,1,4]</div>\n<div>输出: 2</div>\n<div>解释: 跳到最后一个位置的最小跳跃数是 2。</div>\n<div>&nbsp; &nbsp; &nbsp;从下标为 0 跳到下标为 1 的位置，跳 1 步，然后跳 3 步到达数组的最后一个位置。</div>\n<div>示例 2:</div>\n<div>&nbsp;</div>\n<div>输入: nums = [2,3,0,1,4]</div>\n<div>输出: 2</div>\n<div>&nbsp;</div>\n<div>\n<h3>贪心</h3>\n<pre class=\"language-java\"><code>    public int jump(int[] nums) {\n        int res = 0;\n        int endPoint = nums.length - 1;\n        //已知:你总是可以到达数组的最后一个位置,则到达终点那一次尽可能跳的更远\n        while (endPoint != 0) {\n            for (int i = 0; i &lt; nums.length; i++) {\n                //从左开始找,第一个匹配的是当前最优解(跳跃步数多)\n                if (nums[i] + i &gt;= endPoint) {\n                    ++res;\n                    endPoint = i;\n                    break;\n                }\n            }\n        }\n        return res;\n    }</code></pre>\n</div>', '55. 跳跃游戏 45. 跳跃游戏 II', '', 0, 1, 1, 284, 2, '2021-12-30 11:27:02', '2022-03-18 11:46:25');

-- ----------------------------
-- Table structure for t_blog_tag
-- ----------------------------
DROP TABLE IF EXISTS `t_blog_tag`;
CREATE TABLE `t_blog_tag`  (
  `bl_id` int(11) NOT NULL,
  `ta_id` int(11) NOT NULL,
  INDEX `bl_id`(`bl_id`) USING BTREE,
  INDEX `ta_id`(`ta_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_blog_tag
-- ----------------------------
INSERT INTO `t_blog_tag` VALUES (45, 1);
INSERT INTO `t_blog_tag` VALUES (46, 1);
INSERT INTO `t_blog_tag` VALUES (54, 4);
INSERT INTO `t_blog_tag` VALUES (60, 5);
INSERT INTO `t_blog_tag` VALUES (61, 6);
INSERT INTO `t_blog_tag` VALUES (62, 3);
INSERT INTO `t_blog_tag` VALUES (63, 5);
INSERT INTO `t_blog_tag` VALUES (65, 4);
INSERT INTO `t_blog_tag` VALUES (68, 4);
INSERT INTO `t_blog_tag` VALUES (47, 2);
INSERT INTO `t_blog_tag` VALUES (47, 3);
INSERT INTO `t_blog_tag` VALUES (69, 3);
INSERT INTO `t_blog_tag` VALUES (69, 2);
INSERT INTO `t_blog_tag` VALUES (70, 7);
INSERT INTO `t_blog_tag` VALUES (71, 7);
INSERT INTO `t_blog_tag` VALUES (66, 4);
INSERT INTO `t_blog_tag` VALUES (67, 4);
INSERT INTO `t_blog_tag` VALUES (64, 4);
INSERT INTO `t_blog_tag` VALUES (75, 4);
INSERT INTO `t_blog_tag` VALUES (76, 7);
INSERT INTO `t_blog_tag` VALUES (88, 2);
INSERT INTO `t_blog_tag` VALUES (89, 5);
INSERT INTO `t_blog_tag` VALUES (90, 10);
INSERT INTO `t_blog_tag` VALUES (59, 10);
INSERT INTO `t_blog_tag` VALUES (91, 10);
INSERT INTO `t_blog_tag` VALUES (96, 3);
INSERT INTO `t_blog_tag` VALUES (96, 4);
INSERT INTO `t_blog_tag` VALUES (99, 5);
INSERT INTO `t_blog_tag` VALUES (100, 10);
INSERT INTO `t_blog_tag` VALUES (106, 11);
INSERT INTO `t_blog_tag` VALUES (108, 4);
INSERT INTO `t_blog_tag` VALUES (109, 14);
INSERT INTO `t_blog_tag` VALUES (109, 13);
INSERT INTO `t_blog_tag` VALUES (109, 12);
INSERT INTO `t_blog_tag` VALUES (110, 2);
INSERT INTO `t_blog_tag` VALUES (110, 3);
INSERT INTO `t_blog_tag` VALUES (110, 15);
INSERT INTO `t_blog_tag` VALUES (111, 2);
INSERT INTO `t_blog_tag` VALUES (111, 1);
INSERT INTO `t_blog_tag` VALUES (111, 15);
INSERT INTO `t_blog_tag` VALUES (112, 2);
INSERT INTO `t_blog_tag` VALUES (112, 3);
INSERT INTO `t_blog_tag` VALUES (113, 4);
INSERT INTO `t_blog_tag` VALUES (114, 1);
INSERT INTO `t_blog_tag` VALUES (115, 4);
INSERT INTO `t_blog_tag` VALUES (116, 4);
INSERT INTO `t_blog_tag` VALUES (119, 13);
INSERT INTO `t_blog_tag` VALUES (120, 16);
INSERT INTO `t_blog_tag` VALUES (120, 13);
INSERT INTO `t_blog_tag` VALUES (121, 15);
INSERT INTO `t_blog_tag` VALUES (122, 3);
INSERT INTO `t_blog_tag` VALUES (123, 4);
INSERT INTO `t_blog_tag` VALUES (124, 13);
INSERT INTO `t_blog_tag` VALUES (125, 4);
INSERT INTO `t_blog_tag` VALUES (127, 1);
INSERT INTO `t_blog_tag` VALUES (128, 18);
INSERT INTO `t_blog_tag` VALUES (129, 15);
INSERT INTO `t_blog_tag` VALUES (130, 3);
INSERT INTO `t_blog_tag` VALUES (131, 4);
INSERT INTO `t_blog_tag` VALUES (117, 3);
INSERT INTO `t_blog_tag` VALUES (107, 3);
INSERT INTO `t_blog_tag` VALUES (134, 19);
INSERT INTO `t_blog_tag` VALUES (133, 4);

-- ----------------------------
-- Table structure for t_comment
-- ----------------------------
DROP TABLE IF EXISTS `t_comment`;
CREATE TABLE `t_comment`  (
  `co_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '评论者姓名',
  `email` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '邮箱',
  `content` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '内容',
  `is_admin` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否是博主',
  `is_delete` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否逻辑删除',
  `bl_id` int(11) NOT NULL COMMENT '博客id',
  `parent_id` int(11) NULL DEFAULT NULL COMMENT '父节点',
  `ip_address` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'IP地址',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`co_id`) USING BTREE,
  INDEX `bl_id`(`bl_id`) USING BTREE,
  INDEX `comment_ibfk_2`(`parent_id`) USING BTREE,
  INDEX `comment_ibfk_3`(`ip_address`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 190 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_comment
-- ----------------------------
INSERT INTO `t_comment` VALUES (1, '游客one', '1333333@qq.com', '文章不错点赞！', 1, 0, 1, NULL, '127.0.0.1', '2019-06-23 13:09:35', '2020-04-01 17:37:18');
INSERT INTO `t_comment` VALUES (2, '游客two', '456798@qqcom', '随便说点什么吧！！！', 0, 0, 1, 1, '127.0.0.1', '2019-06-23 13:10:09', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (3, '雷浩', '123456@qq.com', '内容内容内容内容内容内容', 0, 0, 1, NULL, '127.0.0.1', '2019-06-24 00:00:00', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (4, '测试', '123@qq.com', 'ceshi', 0, 0, 1, 1, '127.0.0.1', '2019-11-16 05:51:01', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (5, '测试二', '111@qq.com', '哈哈', 0, 0, 1, 1, '127.0.0.1', '2019-11-16 05:51:01', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (6, '王五', '555@qq.com', '回复雷浩', 0, 0, 1, 3, '127.0.0.1', '2019-11-09 12:25:05', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (7, '阿斯', '2910819096@qq.com', '66666', 0, 0, 1, 3, '127.0.0.1', '2019-11-09 12:28:51', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (8, '77', '775736156@qq.com', '777', 0, 0, 1, 3, '127.0.0.1', '2019-11-09 12:30:43', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (9, '888', '365651975@qq.com', '回复77', 0, 0, 1, 8, '127.0.0.1', '2019-11-09 12:31:57', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (12, '阿飞', '775736156@qq.com', '啊哈哈', 0, 0, 1, NULL, '127.0.0.1', '2019-11-10 01:12:44', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (13, '爱迪生', '365651975@qq.com', '6', 0, 0, 1, 12, '127.0.0.1', '2019-11-10 01:13:42', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (14, '阿斯达', '365651975@qq.com', '你好爱迪生', 0, 0, 1, 13, '127.0.0.1', '2019-11-10 01:13:56', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (15, '张无忌', '775736156@qq.com', '比较明显的评论！！！！！！！！！！！！！！', 0, 0, 1, 2, '127.0.0.1', '2019-11-12 09:04:52', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (16, '赵敏', '2910819096@qq.com', '我是赵敏啊！', 0, 0, 1, 15, '127.0.0.1', '2019-11-12 09:08:55', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (17, '张无忌', '1143038749@qq.com', '优秀啊 优秀', 0, 0, 1, 16, '127.0.0.1', '2019-11-12 09:12:40', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (18, '路人哈哈', '775736156@qq.com', '你们都这么厉害吗', 0, 0, 1, NULL, '127.0.0.1', '2019-11-12 11:58:23', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (25, 'nickname', '123@qq.com', '测试的', 0, 0, 1, NULL, '127.0.0.1', '2019-11-18 09:48:27', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (26, '555', '775736156@qq.com', '12312321312312', 0, 0, 1, NULL, '127.0.0.1', '2019-11-18 17:06:40', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (27, '555', '775736156@qq.com', '55555555555555555', 0, 0, 1, NULL, '127.0.0.1', '2019-11-18 17:07:57', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (28, '555', '775736156@qq.com', '321', 0, 0, 1, NULL, '127.0.0.1', '2019-11-18 17:08:27', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (33, '测试人员', '666@qq.com', '随便说点', 0, 0, 1, NULL, '127.0.0.1', '2019-11-22 18:48:30', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (34, '测试人员', '666@qq.com', '谁啊', 0, 0, 12, NULL, '127.0.0.1', '2019-11-22 18:49:33', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (35, '测试人员', '666@qq.com', '厉害', 0, 0, 12, 34, '127.0.0.1', '2019-11-22 18:49:41', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (36, '测试人员', '666@qq.com', '努伊斯', 0, 0, 12, 35, '127.0.0.1', '2019-11-22 18:49:48', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (37, '哈哈', '111@qq.com', '来来来', 0, 1, 1, NULL, '127.0.0.1', '2019-11-24 10:13:33', '2022-03-18 14:03:08');
INSERT INTO `t_comment` VALUES (38, '哈哈', '111@qq.com', '66666', 0, 0, 4, NULL, '127.0.0.1', '2019-11-24 10:14:01', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (39, '张三', '775736156@qq.com', '牛皮', 0, 0, 4, NULL, '127.0.0.1', '2019-11-26 20:26:14', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (40, '游客', '114@qq.com', '啊哈', 0, 0, 59, NULL, '127.0.0.1', '2019-12-25 11:14:30', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (41, '张三', '114@qq.com', '嘞好啊ớ ₃ờ', 0, 0, 59, 40, '127.0.0.1', '2019-12-25 11:14:58', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (42, '李四', '114@qq.com', '哈哈哈', 0, 0, 59, 41, '127.0.0.1', '2019-12-25 11:15:24', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (43, '测试人员', '666@qq.com', '嘤嘤嘤', 0, 0, 1, 2, '127.0.0.1', '2020-01-08 16:51:48', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (44, '张三', '111@qq.com', '哈哈哈', 0, 0, 68, NULL, '127.0.0.1', '2020-01-20 22:38:47', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (45, '李四', '111@qq.com', '你好张三', 0, 0, 68, 44, '127.0.0.1', '2020-01-20 22:39:28', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (46, '张三', '111@qq.com', '你也好', 0, 0, 68, 45, '127.0.0.1', '2020-01-20 22:39:51', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (47, '王五', '111@qq.com', '啊啊啊', 0, 0, 68, NULL, '127.0.0.1', '2020-01-20 22:41:07', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (48, '66', '66', '66', 0, 0, 69, NULL, '127.0.0.1', '2020-01-31 15:51:12', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (49, '请问', '1346138651@qq.com', 'q\'q\'we', 0, 0, 70, NULL, '127.0.0.1', '2020-02-08 15:02:49', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (50, '张三', '1', '你好', 0, 0, 69, 48, '127.0.0.1', '2020-02-12 17:17:35', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (51, '123', '123', '123456', 0, 0, 57, NULL, '127.0.0.1', '2020-03-27 09:28:12', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (54, '321', '123', '321', 0, 0, 57, NULL, '127.0.0.1', '2020-03-27 09:32:43', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (56, '77', '123', '123123', 0, 0, 76, NULL, '127.0.0.1', '2020-03-31 20:36:09', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (57, 'a', '123@qq.com', '123 hi', 0, 0, 57, 51, '127.0.0.1', '2020-04-01 14:04:51', '2020-04-01 17:37:01');
INSERT INTO `t_comment` VALUES (58, '喂喂喂', '111', '你好(*´▽｀)ノノ', 0, 0, 46, NULL, '183.252.239.82', '2020-04-07 23:07:12', '2020-04-07 23:07:12');
INSERT INTO `t_comment` VALUES (59, 'admin', '213', '不错不错', 0, 0, 91, NULL, '49.67.62.168', '2020-04-14 12:09:25', '2020-04-14 12:09:25');
INSERT INTO `t_comment` VALUES (60, 'admin', '213', '测试一下二级回复', 0, 0, 91, 59, '49.67.62.168', '2020-04-14 12:09:41', '2020-04-14 12:09:41');
INSERT INTO `t_comment` VALUES (61, 'demo', '23354@qq.com', '222', 0, 0, 90, NULL, '39.158.164.202', '2020-04-22 20:07:36', '2020-04-22 20:07:36');
INSERT INTO `t_comment` VALUES (62, '123', '123@qq.com', '测试', 0, 0, 91, NULL, '101.24.57.40', '2020-04-23 12:08:02', '2020-04-23 12:08:02');
INSERT INTO `t_comment` VALUES (63, '123124', '1', '111', 0, 1, 91, NULL, '120.206.106.68', '2020-05-03 10:50:27', '2020-05-11 14:22:34');
INSERT INTO `t_comment` VALUES (64, 'test', '2910819096@qq.com', '你好啊', 0, 0, 91, 60, '101.24.58.88', '2020-05-11 14:21:10', '2020-05-11 14:21:10');
INSERT INTO `t_comment` VALUES (65, 'test', '2910819096@qq.com', 'hello', 0, 0, 91, 62, '101.24.58.88', '2020-05-11 14:21:21', '2020-05-11 14:21:21');
INSERT INTO `t_comment` VALUES (66, 'test', '2910819096@qq.com', '我是四楼 哈哈！', 0, 0, 91, NULL, '101.24.58.88', '2020-05-11 14:21:37', '2020-05-11 14:21:37');
INSERT INTO `t_comment` VALUES (67, '张三', '123', '内容不错，学习一波', 0, 0, 93, NULL, '101.24.58.88', '2020-05-23 10:58:58', '2020-05-23 10:58:58');
INSERT INTO `t_comment` VALUES (68, '李四', '123', '你好，张三', 0, 0, 93, 67, '101.24.58.88', '2020-05-23 10:59:11', '2020-05-23 10:59:11');
INSERT INTO `t_comment` VALUES (69, '你好呀', '@', '测试', 0, 0, 93, NULL, '220.168.52.218', '2020-06-05 19:59:57', '2020-06-05 19:59:57');
INSERT INTO `t_comment` VALUES (70, '你好呀', '@', '你好呀', 0, 0, 93, NULL, '220.168.52.218', '2020-06-05 20:00:57', '2020-06-05 20:00:57');
INSERT INTO `t_comment` VALUES (71, '222', '3333', '1111', 0, 0, 93, NULL, '112.2.93.227', '2020-07-24 16:33:03', '2020-07-24 16:33:03');
INSERT INTO `t_comment` VALUES (72, '11', '11', '11', 0, 0, 98, NULL, '117.60.29.143', '2020-07-26 17:22:07', '2020-07-26 17:22:07');
INSERT INTO `t_comment` VALUES (73, '11', '11', '111', 0, 0, 98, 72, '117.60.29.143', '2020-07-26 17:22:12', '2020-07-26 17:22:12');
INSERT INTO `t_comment` VALUES (74, '11', '11', '111', 0, 0, 98, 73, '117.60.29.143', '2020-07-26 17:22:18', '2020-07-26 17:22:18');
INSERT INTO `t_comment` VALUES (75, 'tert', '2423@qq.com', 'eqweqwe', 0, 0, 98, NULL, '111.227.125.43', '2020-07-28 23:53:27', '2020-07-28 23:53:27');
INSERT INTO `t_comment` VALUES (76, '1171205514', 'pli38546@gmail.com', '品论系统怎么做的？', 0, 0, 98, NULL, '113.89.99.39', '2020-08-06 16:12:45', '2020-08-06 16:12:45');
INSERT INTO `t_comment` VALUES (77, '1171205514', 'pli38546@gmail.com', '测试一下', 0, 0, 98, 76, '113.89.99.39', '2020-08-06 16:12:58', '2020-08-06 16:12:58');
INSERT INTO `t_comment` VALUES (78, '张三', 'pli38546@gmail.com', '你好，张三', 0, 0, 98, NULL, '113.89.99.39', '2020-08-06 16:14:14', '2020-08-06 16:14:14');
INSERT INTO `t_comment` VALUES (79, '李四', '1171205514@qq.com', '你好，你吃法了', 0, 0, 98, 78, '113.89.99.39', '2020-08-06 16:14:55', '2020-08-06 16:14:55');
INSERT INTO `t_comment` VALUES (80, '张三', '1171205514@qq.com', '我还没有呢？你呢', 0, 0, 98, 79, '113.89.99.39', '2020-08-06 16:15:35', '2020-08-06 16:15:35');
INSERT INTO `t_comment` VALUES (81, '李四', '1171205514@qq.com', '我刚吃了', 0, 0, 98, 80, '113.89.99.39', '2020-08-06 16:16:11', '2020-08-06 16:16:11');
INSERT INTO `t_comment` VALUES (82, '王五', '1171205514@qq.com', '你们在聊什么？带带我', 0, 0, 98, 78, '113.89.99.39', '2020-08-06 16:17:19', '2020-08-06 16:17:19');
INSERT INTO `t_comment` VALUES (83, '王五', '1171205514@qq.com', '李四，你吃了啥？', 0, 0, 98, 79, '113.89.99.39', '2020-08-06 16:18:04', '2020-08-06 16:18:04');
INSERT INTO `t_comment` VALUES (84, '大大', '大大', '吊袜带', 0, 1, 98, NULL, '113.250.234.48', '2020-08-12 19:44:43', '2020-08-26 15:40:40');
INSERT INTO `t_comment` VALUES (85, '大大', '大大', '大大大大', 0, 0, 98, 84, '113.250.234.48', '2020-08-12 19:44:53', '2020-08-12 19:44:53');
INSERT INTO `t_comment` VALUES (86, '大大', '大大', '大大大大', 0, 0, 98, 85, '113.250.234.48', '2020-08-12 19:44:59', '2020-08-12 19:44:59');
INSERT INTO `t_comment` VALUES (87, 'kiuk', 'ki', 'mjumj', 0, 0, 92, NULL, '219.228.146.42', '2020-08-14 20:16:00', '2020-08-14 20:16:00');
INSERT INTO `t_comment` VALUES (88, 'fg', 'fg@qq.com', 'fg', 0, 0, 92, NULL, '114.250.138.161', '2020-09-09 21:17:34', '2020-09-09 21:17:34');
INSERT INTO `t_comment` VALUES (89, 'fg', 'fg@qq.com', 'ffffffffffffffffff', 0, 0, 92, NULL, '114.250.138.161', '2020-09-09 21:17:40', '2020-09-09 21:17:40');
INSERT INTO `t_comment` VALUES (90, 'fg', 'fg@qq.com', 'fffffffffffffffffffffffffffffff', 0, 0, 92, NULL, '114.250.138.161', '2020-09-09 21:17:45', '2020-09-09 21:17:45');
INSERT INTO `t_comment` VALUES (91, 'lovepli', 'pli38546@gmail.com', '测试一下评论功能lovepli', 0, 0, 100, NULL, '113.89.97.1', '2020-09-14 10:37:15', '2020-09-14 10:37:15');
INSERT INTO `t_comment` VALUES (92, 'lovepli', 'pli38546@gmail.com', '你是老李？？？', 0, 0, 100, 91, '113.89.97.1', '2020-09-14 10:38:31', '2020-09-14 10:38:31');
INSERT INTO `t_comment` VALUES (93, 'lovepli555', 'pli38546@gmail.com', '你是？？', 0, 0, 100, NULL, '183.11.131.66', '2020-09-14 10:43:14', '2020-09-14 10:43:14');
INSERT INTO `t_comment` VALUES (94, '测试', 'xfgdbmb@qq.com', 'dxgxd', 0, 0, 100, NULL, '182.90.73.53', '2020-09-18 00:47:31', '2020-09-18 00:47:31');
INSERT INTO `t_comment` VALUES (95, 'admin', 'lvkf@163.com', '哈哈哈哈哈哈', 0, 0, 100, 92, '60.12.1.210', '2020-09-22 12:04:12', '2020-09-22 12:04:12');
INSERT INTO `t_comment` VALUES (96, '急急急', '1', '怕【p\'m', 0, 0, 98, NULL, '117.154.66.201', '2020-10-19 18:33:48', '2020-10-19 18:33:48');
INSERT INTO `t_comment` VALUES (97, '追风筝的人', '117997@qq.com', '阿萨大', 0, 0, 48, NULL, '116.228.50.38', '2020-10-28 10:14:02', '2020-10-28 10:14:02');
INSERT INTO `t_comment` VALUES (98, '追风筝的人', '117997@qq.com', '阿达撒答', 0, 0, 48, NULL, '116.228.50.38', '2020-10-28 10:14:11', '2020-10-28 10:14:11');
INSERT INTO `t_comment` VALUES (99, '追风筝的人', '117997@qq.com', '阿萨大', 0, 0, 48, NULL, '116.228.50.38', '2020-10-28 10:14:35', '2020-10-28 10:14:35');
INSERT INTO `t_comment` VALUES (100, '霸哥', '123456@qq.com', '强啊，霸哥！', 0, 0, 104, NULL, '119.253.42.52', '2020-11-19 18:46:34', '2020-11-19 18:48:16');
INSERT INTO `t_comment` VALUES (101, '游客139059144', 'wanglidanya@163.com', 'hhhh', 0, 0, 104, 100, '221.192.179.98', '2020-12-08 20:17:35', '2020-12-08 20:17:35');
INSERT INTO `t_comment` VALUES (102, '撒旦飞洒地方', 'asdfsad@qq.com', '\n\'\n\'\'\'\'\';;\';\';\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\';\';\';\'', 0, 0, 104, NULL, '27.38.4.73', '2020-12-16 17:46:02', '2020-12-16 17:46:02');
INSERT INTO `t_comment` VALUES (103, '撒旦飞洒地方', 'asdfsad@qq.com', 'k;kk;lkl;;lk;kl;kl;', 0, 0, 104, 100, '27.38.4.73', '2020-12-16 17:46:20', '2020-12-16 17:46:20');
INSERT INTO `t_comment` VALUES (104, '撒旦飞洒地方', 'asdfsad@qq.com', 'ftghjkl;', 0, 0, 104, NULL, '27.38.4.73', '2020-12-16 17:46:31', '2020-12-16 17:46:31');
INSERT INTO `t_comment` VALUES (105, '撒旦飞洒地方', 'asdfsad@qq.com', 'k;lkl;kl;k;lkl;l;', 0, 0, 104, 103, '27.38.4.73', '2020-12-16 17:46:41', '2020-12-16 17:46:41');
INSERT INTO `t_comment` VALUES (106, '撒旦飞洒地方', 'asdfsad@qq.com', 'klkl;k;kl;kl', 0, 0, 104, 103, '27.38.4.73', '2020-12-16 17:46:51', '2020-12-16 17:46:51');
INSERT INTO `t_comment` VALUES (107, '111', '222', '<script>alert(1)</script>', 0, 0, 104, NULL, '120.236.15.84', '2020-12-31 16:43:46', '2020-12-31 16:43:46');
INSERT INTO `t_comment` VALUES (108, 'ggg', 'dfs@test.com', '<span style=\"color: red;\">ggg</span>', 0, 0, 104, NULL, '34.96.173.69', '2021-01-18 20:56:41', '2021-01-18 20:56:41');
INSERT INTO `t_comment` VALUES (109, 'demo', 'demo@demo.com', 'hello world', 0, 0, 105, NULL, '106.38.58.200', '2021-02-08 17:24:04', '2021-02-08 17:24:04');
INSERT INTO `t_comment` VALUES (110, 'dd', 'mds118@163.com', '？？？？？？？？？？？？\n', 0, 0, 59, NULL, '183.226.46.43', '2021-03-10 22:27:41', '2021-03-10 22:27:41');
INSERT INTO `t_comment` VALUES (111, 'test', '2910819096@qq.com', 'hi', 0, 0, 105, 109, '120.244.224.76', '2021-03-28 22:35:28', '2021-03-28 22:35:28');
INSERT INTO `t_comment` VALUES (112, '王', 'aa@store.com', '你好', 0, 0, 105, NULL, '114.106.177.213', '2021-04-02 17:12:20', '2021-04-02 17:12:20');
INSERT INTO `t_comment` VALUES (113, '王', 'aa@store.com', '你好', 0, 0, 105, NULL, '114.106.177.213', '2021-04-02 17:12:28', '2021-04-02 17:12:28');
INSERT INTO `t_comment` VALUES (114, 'wang', 'aa@store.com', 'hello', 0, 0, 105, NULL, '114.106.177.213', '2021-04-02 17:15:13', '2021-04-02 17:15:13');
INSERT INTO `t_comment` VALUES (115, 'wang ', 'aa@store.com', 'hi', 0, 0, 105, NULL, '114.106.177.213', '2021-04-02 17:24:08', '2021-04-02 17:24:08');
INSERT INTO `t_comment` VALUES (116, 'wang ', 'aa@store.com', '您好', 0, 0, 105, 111, '114.106.177.213', '2021-04-02 17:24:25', '2021-04-02 17:24:25');
INSERT INTO `t_comment` VALUES (117, 'lisi', 'zzy13652840002@163.com', '哈哈哈哈哈哈', 0, 0, 103, NULL, '183.236.187.214', '2021-04-22 12:13:34', '2021-04-22 12:13:34');
INSERT INTO `t_comment` VALUES (118, '微软', '的反对广泛', '安慰生日过得挺好', 0, 0, 98, NULL, '27.38.254.51', '2021-04-28 22:47:39', '2021-04-28 22:47:39');
INSERT INTO `t_comment` VALUES (119, '微软', '的反对广泛', 'adaw', 0, 0, 98, 118, '27.38.254.51', '2021-04-28 22:48:02', '2021-04-28 22:48:02');
INSERT INTO `t_comment` VALUES (120, 'ffff', '271498689@qq.com', 'fdsafesdfdssf', 0, 0, 105, NULL, '183.192.79.193', '2021-05-06 20:53:56', '2021-05-06 20:53:56');
INSERT INTO `t_comment` VALUES (121, 'sdad', 'asd', 'asdasd', 0, 0, 104, NULL, '43.129.70.201', '2021-05-08 00:22:35', '2021-05-08 00:22:35');
INSERT INTO `t_comment` VALUES (122, 'sdad111', 'asd', '1111', 0, 0, 104, NULL, '43.129.70.201', '2021-05-08 00:22:42', '2021-05-08 00:22:42');
INSERT INTO `t_comment` VALUES (123, 'xxx', '555', 'ff', 0, 0, 105, NULL, '110.52.210.78', '2021-05-16 11:09:50', '2021-05-16 11:09:50');
INSERT INTO `t_comment` VALUES (124, 'care', '1262254804@qq.com', '博主，网站显示    LOGIN_CODE失效了,请告知管理员', 0, 0, 105, NULL, '171.12.94.25', '2021-05-21 10:07:45', '2021-05-21 10:07:45');
INSERT INTO `t_comment` VALUES (125, 'NilBrains', 'me@nilbrains.com', '666', 0, 0, 69, NULL, '39.183.180.71', '2021-05-24 20:54:51', '2021-05-24 20:54:51');
INSERT INTO `t_comment` VALUES (126, '帆帆帆帆', 'ffff@163.com', '飞洒发', 0, 0, 104, NULL, '113.110.166.106', '2021-05-28 11:24:43', '2021-05-28 11:24:43');
INSERT INTO `t_comment` VALUES (127, '帆帆帆帆', 'ffff@163.com', '是么', 0, 0, 104, 103, '113.110.166.106', '2021-05-28 11:26:33', '2021-05-28 11:26:33');
INSERT INTO `t_comment` VALUES (128, '123', '123123', '1231', 0, 0, 103, NULL, '112.4.44.22', '2021-05-28 23:26:48', '2021-05-28 23:26:48');
INSERT INTO `t_comment` VALUES (129, '路人鹏', 'x', '该问题已修复，可以使用了', 0, 0, 105, 124, '120.244.224.179', '2021-06-05 07:48:18', '2021-06-05 07:48:18');
INSERT INTO `t_comment` VALUES (130, 'care', '1262254804@qq.com', '博主，还是有问题，有很大的空，无法下载', 0, 0, 105, 129, '1.196.153.119', '2021-06-06 22:00:57', '2021-06-06 22:00:57');
INSERT INTO `t_comment` VALUES (131, 'system', '123', '666', 0, 0, 100, NULL, '27.129.129.229', '2021-06-11 11:31:47', '2021-06-11 11:31:47');
INSERT INTO `t_comment` VALUES (132, 'system', '123', '666', 0, 0, 105, NULL, '27.129.129.229', '2021-06-11 11:39:44', '2021-06-11 11:39:44');
INSERT INTO `t_comment` VALUES (133, 'dfsdf', '111', '1111', 0, 0, 103, NULL, '113.64.72.214', '2021-06-13 16:32:00', '2021-06-13 16:32:00');
INSERT INTO `t_comment` VALUES (134, 'dfsdf', '770230504@qq.com', '2342342', 0, 0, 103, NULL, '113.64.72.214', '2021-06-13 16:32:11', '2021-06-13 16:32:11');
INSERT INTO `t_comment` VALUES (135, 'dfsdf', '770230504@qq.com', '2342342342', 0, 0, 103, NULL, '113.64.72.214', '2021-06-13 16:32:17', '2021-06-13 16:32:17');
INSERT INTO `t_comment` VALUES (136, '1', '1', '1', 0, 0, 105, NULL, '113.64.75.127', '2021-06-13 19:46:22', '2021-06-13 19:46:22');
INSERT INTO `t_comment` VALUES (137, 'a', '7', 'a', 0, 0, 105, 136, '223.104.64.163', '2021-06-13 20:46:01', '2021-06-13 20:46:01');
INSERT INTO `t_comment` VALUES (138, '宿舍', '宿舍', '嗯嗯', 0, 0, 105, NULL, '113.64.72.214', '2021-06-13 20:46:09', '2021-06-13 20:46:09');
INSERT INTO `t_comment` VALUES (139, 'Jinq Li', 'yyuant@outlook.com', 'gg ', 0, 0, 105, 137, '36.152.116.207', '2021-06-17 12:47:04', '2021-06-17 12:47:04');
INSERT INTO `t_comment` VALUES (140, '5880円/15点', 'aston_martin@aston.com', '23323', 0, 0, 109, NULL, '123.52.16.73', '2021-08-12 22:24:52', '2021-08-12 22:24:52');
INSERT INTO `t_comment` VALUES (141, '1', '2', '121', 0, 0, 106, NULL, '103.251.112.143', '2021-08-18 20:10:06', '2021-08-18 20:10:06');
INSERT INTO `t_comment` VALUES (142, 'one one', '22', '2222', 0, 0, 93, NULL, '111.18.56.219', '2021-08-21 11:23:48', '2021-08-21 11:23:48');
INSERT INTO `t_comment` VALUES (143, 'one one', '22', '22', 0, 0, 93, NULL, '111.18.56.219', '2021-08-21 11:23:53', '2021-08-21 11:23:53');
INSERT INTO `t_comment` VALUES (144, 'one one', '1729401832@qq.com', '邮箱格式没有验证?', 0, 0, 93, NULL, '111.18.56.219', '2021-08-21 11:24:34', '2021-08-21 11:24:34');
INSERT INTO `t_comment` VALUES (145, '强强鹏', '1143828481@qq.com', '写的不戳!', 0, 0, 46, NULL, '120.244.224.6', '2021-08-24 21:08:32', '2021-08-24 21:08:32');
INSERT INTO `t_comment` VALUES (146, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:18', '2021-09-02 08:24:18');
INSERT INTO `t_comment` VALUES (147, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:19', '2021-09-02 08:24:19');
INSERT INTO `t_comment` VALUES (148, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:20', '2021-09-02 08:24:20');
INSERT INTO `t_comment` VALUES (149, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:22', '2021-09-02 08:24:22');
INSERT INTO `t_comment` VALUES (150, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:23', '2021-09-02 08:24:23');
INSERT INTO `t_comment` VALUES (151, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:25', '2021-09-02 08:24:25');
INSERT INTO `t_comment` VALUES (152, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:29', '2021-09-02 08:24:29');
INSERT INTO `t_comment` VALUES (153, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:34', '2021-09-02 08:24:34');
INSERT INTO `t_comment` VALUES (154, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:38', '2021-09-02 08:24:38');
INSERT INTO `t_comment` VALUES (155, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:41', '2021-09-02 08:24:41');
INSERT INTO `t_comment` VALUES (156, 'Old.铁', 'Good@163.com', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:45', '2021-09-02 08:24:45');
INSERT INTO `t_comment` VALUES (157, 'Old.铁', 'Good@163.com', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:49', '2021-09-02 08:24:49');
INSERT INTO `t_comment` VALUES (158, 'Old.铁', 'Good@163.com', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:24:59', '2021-09-02 08:24:59');
INSERT INTO `t_comment` VALUES (159, 'Old.铁', 'Good', 'Good，每天背一个算法题', 0, 0, 122, NULL, '223.104.40.113', '2021-09-02 08:25:07', '2021-09-02 08:25:07');
INSERT INTO `t_comment` VALUES (160, '沦陷nd眼眸', '979780632@qq.com', '过来看看', 0, 0, 123, NULL, '36.112.50.237', '2021-09-09 15:56:05', '2021-09-09 15:56:05');
INSERT INTO `t_comment` VALUES (161, '沦陷nd眼眸', '979780632@qq.com', '在来一趟', 0, 0, 123, NULL, '36.112.50.237', '2021-09-09 15:56:24', '2021-09-09 15:56:24');
INSERT INTO `t_comment` VALUES (162, '沦陷nd眼眸', '979780632@qq.com', '自娱自乐', 0, 0, 123, 161, '36.112.49.237', '2021-09-09 15:56:34', '2021-09-09 15:56:34');
INSERT INTO `t_comment` VALUES (163, '吃薯片', '他天天', '查看原文很good', 0, 0, 124, NULL, '123.119.73.91', '2021-09-09 23:12:56', '2021-09-09 23:12:56');
INSERT INTO `t_comment` VALUES (164, '二哈', 'i', '｡◕‿◕｡', 0, 0, 124, 163, '124.64.19.32', '2021-09-10 11:43:41', '2021-09-10 11:43:41');
INSERT INTO `t_comment` VALUES (165, 'coinker', 'test@test.com', 'good', 0, 0, 127, NULL, '61.149.179.190', '2021-09-29 12:03:15', '2021-09-29 12:03:15');
INSERT INTO `t_comment` VALUES (166, '4646', '45646', '4564564564', 0, 0, 106, NULL, '112.231.43.74', '2021-10-11 10:42:57', '2021-10-11 10:42:57');
INSERT INTO `t_comment` VALUES (167, '/', 'tianzhipeng96@163.com', '不错', 0, 0, 131, NULL, '123.139.20.203', '2021-10-25 10:36:28', '2021-10-25 10:36:28');
INSERT INTO `t_comment` VALUES (168, '获取ip', 'f\'n\'f', '22', 0, 0, 131, 167, '117.136.117.167', '2021-10-28 10:51:51', '2021-10-28 10:51:51');
INSERT INTO `t_comment` VALUES (169, '获取ip', 'f\'n\'f', '56', 0, 0, 131, 168, '117.136.117.167', '2021-10-28 10:52:04', '2021-10-28 10:52:04');
INSERT INTO `t_comment` VALUES (170, '获取ip', 'f\'n\'f', '3', 0, 0, 98, 75, '117.136.117.167', '2021-10-28 10:58:12', '2021-10-28 10:58:12');
INSERT INTO `t_comment` VALUES (171, 'Hins', 'Hins.qt@gmail.com', '原来可以评论呀', 0, 0, 131, NULL, '106.81.231.148', '2021-11-24 16:46:53', '2021-11-24 16:46:53');
INSERT INTO `t_comment` VALUES (172, 'qingqiu', '3277408886@qq.com', 'wqewqweqewq', 0, 0, 132, NULL, '120.227.151.236', '2021-11-29 18:57:54', '2021-11-29 18:57:54');
INSERT INTO `t_comment` VALUES (173, '1', '1', '1', 0, 0, 131, 169, '58.20.186.104', '2021-12-02 20:55:11', '2021-12-02 20:55:11');
INSERT INTO `t_comment` VALUES (174, '1', '11', '123', 0, 0, 134, NULL, '117.187.228.97', '2021-12-03 20:55:35', '2021-12-03 20:55:35');
INSERT INTO `t_comment` VALUES (175, '1', '11', '1', 0, 0, 93, NULL, '117.187.228.97', '2021-12-03 20:59:57', '2021-12-03 20:59:57');
INSERT INTO `t_comment` VALUES (176, '张三', '1395923301@qq.com', '111', 0, 0, 134, NULL, '113.57.182.239', '2021-12-16 14:10:30', '2021-12-16 14:10:30');
INSERT INTO `t_comment` VALUES (177, 'gf', '2746550407@qq.com', 'gkggjgjkgkjgjk', 0, 0, 134, NULL, '110.16.109.244', '2021-12-18 21:08:30', '2021-12-18 21:08:30');
INSERT INTO `t_comment` VALUES (178, 'test', '2910819096@qq.com', '哈哈', 0, 0, 105, 112, '223.72.74.59', '2021-12-21 22:43:42', '2021-12-21 22:43:42');
INSERT INTO `t_comment` VALUES (179, 'cui', 'CSP_Coinker@163.com', '学到了！', 0, 0, 137, NULL, '211.94.141.2', '2021-12-23 16:22:38', '2021-12-23 16:22:38');
INSERT INTO `t_comment` VALUES (180, '<script>alert(1)</script>', '<script>alert(1)</script>', '<script>alert(1)</script>', 0, 0, 99, NULL, '121.25.234.2', '2021-12-28 23:40:16', '2021-12-28 23:40:16');
INSERT INTO `t_comment` VALUES (181, 'n', '123456@qq.com', '从', 0, 0, 139, NULL, '120.219.159.136', '2022-01-11 16:17:47', '2022-01-11 16:17:47');
INSERT INTO `t_comment` VALUES (182, 'alert(1)', '1005@163.com', 'alert(1)', 0, 0, 129, NULL, '202.104.160.196', '2022-01-12 19:32:34', '2022-01-12 19:32:34');
INSERT INTO `t_comment` VALUES (183, 'alert(1)', '1005@163.com', '<script>\nlocation.href = \"www.baidu.com\"\n</script>', 0, 0, 129, NULL, '202.104.160.196', '2022-01-12 19:33:37', '2022-01-12 19:33:37');
INSERT INTO `t_comment` VALUES (184, 'a\'s\'d', '123454825@qq.com', '111', 0, 0, 134, NULL, '182.148.56.180', '2022-02-19 14:17:41', '2022-02-19 14:17:41');
INSERT INTO `t_comment` VALUES (185, 'asd', '1651351561@qq.com', 'ewf f sd', 0, 0, 139, NULL, '222.211.204.24', '2022-02-20 20:23:53', '2022-02-20 20:23:53');
INSERT INTO `t_comment` VALUES (186, '逃狱兄弟2', '594379478@qq.com', '1111', 0, 0, 139, NULL, '112.49.161.31', '2022-03-04 14:40:42', '2022-03-04 14:40:42');
INSERT INTO `t_comment` VALUES (187, '逃狱兄弟2', '594379478@qq.com', '2222', 0, 0, 139, NULL, '112.49.100.233', '2022-03-15 16:12:32', '2022-03-15 16:12:32');
INSERT INTO `t_comment` VALUES (188, '2334235', '45345', '好了', 0, 0, 139, NULL, '120.85.113.251', '2022-03-20 21:25:36', '2022-03-20 21:25:36');
INSERT INTO `t_comment` VALUES (189, 'AAA', '1143038749@qq.com', '666', 0, 0, 139, 188, '223.72.80.177', '2022-03-29 22:15:38', '2022-03-29 22:15:38');

-- ----------------------------
-- Table structure for t_friend
-- ----------------------------
DROP TABLE IF EXISTS `t_friend`;
CREATE TABLE `t_friend`  (
  `fr_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '名称',
  `description` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介',
  `website` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '地址',
  `flag` int(2) NOT NULL DEFAULT 1 COMMENT '标识',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`fr_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_friend
-- ----------------------------
INSERT INTO `t_friend` VALUES (1, '纯洁的微笑', '纯洁的微笑大佬的博客', 'http://www.ityouknow.com', 1, '2020-03-21 22:23:25', '2020-03-27 09:49:34');
INSERT INTO `t_friend` VALUES (2, '曾中杰', '曾中杰的个人博客', 'https://www.zengzhongjie.com/', 2, '2020-03-21 22:23:25', '2020-03-21 22:23:25');
INSERT INTO `t_friend` VALUES (3, '寒光博客', '寒光博客', 'https://dxoca.cn/', 2, '2020-05-19 15:40:02', '2020-05-19 15:40:02');
INSERT INTO `t_friend` VALUES (4, '上帝爱吃苹果', '上帝爱吃苹果的个人博客', 'https://www.cnblogs.com/keeya/category/1255597.html', 2, '2020-07-23 19:28:09', '2020-07-23 19:28:09');
INSERT INTO `t_friend` VALUES (5, '石杉笔记', '石杉笔记', 'https://www.imooc.com/u/7694107/articles', 1, '2020-08-31 16:54:51', '2020-08-31 16:54:51');
INSERT INTO `t_friend` VALUES (6, 'weiwei LeetCode大佬', 'weiwei LeetCode大佬', 'https://liweiwei1419.gitee.io/leetcode-algo/', 1, '2021-08-19 21:37:31', '2021-08-19 21:37:31');

-- ----------------------------
-- Table structure for t_request_log
-- ----------------------------
DROP TABLE IF EXISTS `t_request_log`;
CREATE TABLE `t_request_log`  (
  `log_id` bigint(11) NOT NULL AUTO_INCREMENT,
  `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求地址',
  `ip_address` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '访问者ip',
  `class_method` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求类名',
  `args` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求参数',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  PRIMARY KEY (`log_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_request_log
-- ----------------------------

-- ----------------------------
-- Table structure for t_tag
-- ----------------------------
DROP TABLE IF EXISTS `t_tag`;
CREATE TABLE `t_tag`  (
  `ta_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '标签名称',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`ta_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_tag
-- ----------------------------
INSERT INTO `t_tag` VALUES (1, '链表', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (2, '递归', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (3, '回溯', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (4, '动态规划', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (5, 'SQL', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (6, 'AcitveMQ', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (7, 'SpringBoot', '2020-03-21 22:14:52', '2020-03-31 14:34:04');
INSERT INTO `t_tag` VALUES (10, 'Redis', '2020-04-07 18:50:36', '2020-04-07 18:50:36');
INSERT INTO `t_tag` VALUES (11, '位运算', '2021-08-09 19:06:01', '2021-08-09 19:06:01');
INSERT INTO `t_tag` VALUES (12, '滑动窗口', '2021-08-12 22:10:23', '2021-08-12 22:10:23');
INSERT INTO `t_tag` VALUES (13, '双指针', '2021-08-12 22:10:29', '2021-08-12 22:10:29');
INSERT INTO `t_tag` VALUES (14, '哈希表', '2021-08-12 22:10:39', '2021-08-12 22:10:39');
INSERT INTO `t_tag` VALUES (15, '树', '2021-08-15 10:58:55', '2021-08-15 10:58:55');
INSERT INTO `t_tag` VALUES (16, '单调栈', '2021-08-29 22:02:51', '2021-08-29 22:02:51');
INSERT INTO `t_tag` VALUES (18, '排序', '2021-10-01 21:34:42', '2021-10-01 21:34:42');
INSERT INTO `t_tag` VALUES (19, '二分查找', '2021-12-03 16:40:03', '2021-12-03 16:40:03');

-- ----------------------------
-- Table structure for t_type
-- ----------------------------
DROP TABLE IF EXISTS `t_type`;
CREATE TABLE `t_type`  (
  `ty_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '类型名',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`ty_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_type
-- ----------------------------
INSERT INTO `t_type` VALUES (1, 'JUC', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (2, '力扣', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (3, '算法', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (4, '数据库', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (5, 'JVM', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (6, 'Java', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (7, '中间件', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (8, '总结', '2020-06-30 14:14:56', '2020-06-30 14:14:56');
INSERT INTO `t_type` VALUES (9, 'Python', '2020-10-20 14:48:52', '2020-10-20 14:48:52');

SET FOREIGN_KEY_CHECKS = 1;
